Skip to content

fix(graph): gate 3D mode on WebGL2 and contain renderer crashes - #539

Merged
AndresL230 merged 2 commits into
mainfrom
fix/538-graph-webgl-fallback
Aug 13, 2026
Merged

fix(graph): gate 3D mode on WebGL2 and contain renderer crashes#539
AndresL230 merged 2 commits into
mainfrom
fix/538-graph-webgl-fallback

Conversation

@AndresL230

@AndresL230AndresL230 commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Fixes#538.

The bug

A browser without WebGL that lands on the dashboard (or Tree/Learn) with localStorage["sapling.kg.mode"] = "3d" persisted collapses the entire app into the root error fallback ("We hit a snag"), permanently — "Try again" re-throws. Chain:

  1. The KnowledgeGraph wrapper honoured the persisted "3d" unconditionally — no WebGL capability check existed anywhere in the frontend.
  2. react-force-graph-3d mounts its engine synchronously in a useLayoutEffect (react-kapsule), constructing new three.WebGLRenderer(...).
  3. three.js r163+ throwsError creating WebGL context. from the constructor when canvas.getContext("webgl2") returns null.
  4. The nearest boundary was the root one (app/layout.tsx), so the whole tree unmounted.

A profile that toggled 3D on a WebGL-capable browser was locked out of the app the moment WebGL went away (disabled, GPU blocklist, remote desktop, VM). Fresh profiles were unaffected (mode defaults to 2D) — which is why this read as "used to work".

The fix (both in the wrapper, so Dashboard/Tree/Learn are all covered)

  1. WebGL2 capability gate (webgl2Available()): probe canvas.getContext("webgl2") once per wrapper mount (probe context released via WEBGL_lose_context so probes never count toward the per-page context cap). Without WebGL2:
    • a persisted "3d" is ignored, not rewritten — the preference survives for the profile's WebGL-capable browsers;
    • the mode toggle renders disabled with title/aria-label "3D requires WebGL";
    • the storage/custom-event mode-sync paths and the setter are gated identically.
  2. Graph-local error boundary, keyed by mode: any residual renderer crash (context creation failing despite a successful probe — GPU process crash, context-limit exhaustion) degrades to the 2D graph inline and heals the persisted mode to "2d"; the heal flips the boundary key, clearing the error state into a normal 2D render. A (never-observed) 2D crash renders a minimal placeholder instead of remounting the thing that just threw.

Tests

  • Unit (KnowledgeGraph.test.tsx, 6 tests, written first and watched fail): fresh-profile default pin; 3d-honoured-with-WebGL pin; forces-2D-without-WebGL2; toggle disabled with explanatory label; toggle enabled with WebGL; crash containment + mode heal (RED run reproduced the exact production throw escaping uncaught).
  • Promoted journey (e2e/graph-webgl-fallback.spec.ts): init script nulls the webgl/webgl2 branches of getContext (exactly the API three probes; 2D canvas stays real) and seeds the poisoned "3d" mode → dashboard must render the 2D SVG graph, no root fallback, toggle disabled, preference preserved.

Verification

  • vitest run: 72 files / 624+ tests green (against a jsdom that is itself a no-WebGL environment, so every existing screen test now exercises the gated path).
  • tsc --noEmit clean; eslint clean on changed files (no new suppressions).
  • next build clean.
  • Pre-fix RED run (stack cycle 1): with the wrapper fix stashed and the real production build served, the new journey fails — the dashboard collapses into the root fallback exactly as reported. The journey failed at the "We hit a snag" count-0 assertion (exit 1) — the root fallback was live on the page.
  • Post-fix full lane (stack cycle 2): entire Chapter-1 Playwright lane + deterministic oracles green. 43 passed (1.5m), oracles exit 0.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved graph rendering when WebGL2 is unavailable or 3D rendering fails.
    • Automatically falls back to the 2D graph while preserving the saved mode preference.
    • Disabled the 3D toggle when unsupported, with an explanatory message.
    • Added recovery and retry handling for temporary graph rendering failures.
    • Prevented graph errors from disrupting the rest of the dashboard.
  • Tests

    • Added coverage for WebGL detection, fallback behavior, mode synchronization, toggle states, and renderer recovery.
    • Added end-to-end coverage for 2D fallback and supported 3D environments.

A browser without WebGL landing on any graph surface with a persisted
"3d" mode collapsed the whole app into the root error fallback: the
wrapper honoured localStorage unconditionally, three r163+ throws from
the WebGLRenderer constructor in a mount-time layout effect, and the
only boundary above it was the root one.
The wrapper now probes canvas.getContext("webgl2") before honouring a
persisted "3d" (ignoring, not rewriting, the preference), disables the
mode toggle with "3D requires WebGL" when the probe fails, and wraps
the graph slot in a mode-keyed local ErrorBoundary whose fallback
degrades a crashed 3D renderer to the 2D graph and heals the persisted
mode to "2d".
Unit tests were written first and watched fail (the crash test
reproduced the exact production throw escaping uncaught); the promoted
journey simulates a no-WebGL browser via an init-script getContext
override and failed against a pre-fix stack build with the dashboard on
the root fallback, exactly as reported.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 12, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-stagingd6cfaeaCommit Preview URL

Branch Preview URL
Aug 13 2026, 01:07 AM

@supabase

supabaseBot commented Aug 12, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: feb9f6c1-33ca-40db-986b-72181daea902

📥 Commits

Reviewing files that changed from the base of the PR and between ce7c291 and d6cfaea.

📒 Files selected for processing (11)
  • docs/frontend-testids.md
  • frontend/e2e/graph-webgl-fallback.spec.ts
  • frontend/eslint-suppressions.json
  • frontend/src/components/ErrorBoundary.tsx
  • frontend/src/components/graph/KnowledgeGraph.test.tsx
  • frontend/src/components/graph/KnowledgeGraph.tsx
  • frontend/src/components/graph/KnowledgeGraph3D.test.tsx
  • frontend/src/components/graph/KnowledgeGraph3D.testmode.test.tsx
  • frontend/src/components/graph/KnowledgeGraph3D.tsx
  • frontend/src/components/screens/AdminAnalytics.test.tsx
  • frontend/src/test-utils/mockNextDynamic.tsx

📝 Walkthrough

Walkthrough

KnowledgeGraph now detects WebGL2 before selecting 3D mode, contains renderer failures locally, preserves the stored preference, and adds unit and Playwright regression coverage.

Changes

Knowledge graph WebGL fallback

Layer / File(s)Summary
WebGL2 capability and mode gating
frontend/src/components/graph/KnowledgeGraph.tsx, frontend/src/components/graph/KnowledgeGraph3D.tsx
The graph probes WebGL2, derives an effective mode without changing the stored preference, and disables the 3D toggle when WebGL2 is unavailable.
Local renderer failure recovery
frontend/src/components/graph/KnowledgeGraph.tsx, frontend/src/components/ErrorBoundary.tsx, docs/frontend-testids.md
A graph-local boundary shows retryable fallback UI, re-probes after 3D failures, and switches to 2D when capability is lost.
Regression coverage and test infrastructure
frontend/src/components/graph/KnowledgeGraph.test.tsx, frontend/e2e/graph-webgl-fallback.spec.ts, frontend/src/test-utils/mockNextDynamic.tsx, frontend/src/components/graph/*test.tsx, frontend/src/components/screens/AdminAnalytics.test.tsx
Tests cover mode selection, WebGL2 gating, synchronization, toggle accessibility, crash recovery, retries, and persisted preferences. Shared dynamic-import mocking replaces local implementations.

Estimated code review effort: 4 (Complex) | ~45 minutes

Mergeability Score:⚪ Minimal · up to d6cfa

The change gates 3D rendering on WebGL2 and falls back to the 2D graph when renderer creation fails; no actionable merge-blocking risk remains after normal checks and review.

Sequence Diagram(s)

sequenceDiagram
participant Browser
participant KnowledgeGraph
participant localStorage
participant KnowledgeGraph3D
participant KnowledgeGraph2D
Browser->>KnowledgeGraph: mount graph
KnowledgeGraph->>Browser: probe WebGL2
Browser-->>KnowledgeGraph: return capability result
KnowledgeGraph->>localStorage: read persisted mode
KnowledgeGraph->>KnowledgeGraph2D: render effective 2D mode when WebGL2 is unavailable
KnowledgeGraph-->>Browser: expose disabled 3D toggle explanation
KnowledgeGraph->>KnowledgeGraph3D: mount 3D renderer when supported
KnowledgeGraph3D-->>KnowledgeGraph: report renderer failure
KnowledgeGraph->>KnowledgeGraph2D: render local 2D fallback
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 25.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly and concisely identifies the primary WebGL2 gating and renderer crash-containment changes.
Description check✅ PassedThe description explains the bug, implementation, linked issue, tests, and verification, although it does not follow every template heading.
Linked Issues check✅ PassedThe changes satisfy [#538] by gating persisted 3D mode, preserving preferences, containing graph crashes, and adding regression tests.
Out of Scope Changes check✅ PassedAll changes support the WebGL2 fallback, graph crash containment, accessibility, documentation, or related test infrastructure objectives.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/538-graph-webgl-fallback

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/src/components/graph/KnowledgeGraph.tsx`:
- Line 141: In frontend/src/components/graph/KnowledgeGraph.tsx:141-141, wrap
the fallback KnowledgeGraph2D rendered by GraphCrashFallback for crashedMode ===
"3d" in a nested ErrorBoundary using the inline status placeholder as its
fallback, preventing 2D failures from reaching the root fallback. In
frontend/src/components/graph/KnowledgeGraph.test.tsx:30-50, add the throw2d
mock control; in frontend/src/components/graph/KnowledgeGraph.test.tsx:148-176,
add a regression test covering simultaneous 3D and 2D failures and asserting the
nested placeholder is rendered.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5c77b3cc-2ebe-41aa-af76-de2ad1793ce2

📥 Commits

Reviewing files that changed from the base of the PR and between 7681c48 and ce7c291.

📒 Files selected for processing (3)
  • frontend/e2e/graph-webgl-fallback.spec.ts
  • frontend/src/components/graph/KnowledgeGraph.test.tsx
  • frontend/src/components/graph/KnowledgeGraph.tsx

React.useEffect(() => {
if (crashedMode === "3d") heal("2d");
}, [crashedMode, heal]);
if (crashedMode === "3d") return <KnowledgeGraph2D {...graphProps} />;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -e
printf'%s\n''--- KnowledgeGraph.tsx outline ---'
ast-grep outline frontend/src/components/graph/KnowledgeGraph.tsx
printf'%s\n''--- Relevant implementation ---'
sed -n '1,230p' frontend/src/components/graph/KnowledgeGraph.tsx
printf'%s\n''--- Relevant test structure ---'
sed -n '1,230p' frontend/src/components/graph/KnowledgeGraph.test.tsx

Repository: SaplingLearn/Sapling

Length of output: 14232


🏁 Script executed:

#!/bin/bashset -e
printf'%s\n''--- ErrorBoundary definition and usages ---'
fd -i 'ErrorBoundary' frontend/src
rg -n -C 12 'class ErrorBoundary|function ErrorBoundary|<ErrorBoundary' frontend/src
printf'%s\n''--- GraphCrashFallback references ---'
rg -n -C 8 'GraphCrashFallback|crashedMode === "3d"|The graph couldn' frontend/src/components/graph

Repository: SaplingLearn/Sapling

Length of output: 10536


🏁 Script executed:

#!/bin/bashset -e
cat -n frontend/src/components/ErrorBoundary.tsx
printf'%s\n''--- package and test scripts ---'if [ -f frontend/package.json ];then
sed -n '1,220p' frontend/package.json
fiif [ -f package.json ];then
sed -n '1,180p' package.json
fi

Repository: SaplingLearn/Sapling

Length of output: 7291


🏁 Script executed:

#!/bin/bashset -e
python3 - <<'PY'from pathlib import Pathimpl = Path("frontend/src/components/graph/KnowledgeGraph.tsx").read_text()boundary = Path("frontend/src/components/ErrorBoundary.tsx").read_text()test = Path("frontend/src/components/graph/KnowledgeGraph.test.tsx").read_text()checks = { "outer boundary invokes GraphCrashFallback as its fallback": ( '<GraphCrashFallback crashedMode={mode} graphProps={props} heal={setMode} />' in impl ), "3D fallback directly renders 2D": ( 'if (crashedMode === "3d") return <KnowledgeGraph2D {...graphProps} />;' in impl ), "ErrorBoundary calls fallback from its own render": ( "if (this.props.fallback) return this.props.fallback(this.state.error, this.reset);" in boundary ), "2D mock has no throw2d control": ( "throw2d" not in test ), "existing crash test only arms 3D": ( "mockState.throw3d = true;" in test and "mockState.throw2d" not in test ),}for name, result in checks.items(): print(f"{'PASS' if result else 'FAIL'}: {name}")# This models the relevant React error-boundary rule:# an error thrown while an ErrorBoundary renders its own fallback# cannot be handled by that same boundary.print("MODEL: 3D child throws -> outer boundary renders GraphCrashFallback")print("MODEL: GraphCrashFallback renders 2D directly -> a 2D throw occurs in outer fallback render")print("MODEL: without a nested boundary, that throw propagates to the next ancestor boundary")PY

Repository: SaplingLearn/Sapling

Length of output: 637


Contain a 2D failure during the 3D fallback.

When KnowledgeGraph3D throws, GraphCrashFallback renders KnowledgeGraph2D from the outer ErrorBoundary fallback. A 2D error then propagates to the root fallback. Wrap the fallback 2D graph in a nested ErrorBoundary with the inline status placeholder as its fallback. Add a throw2d mock control and regression test for simultaneous 3D and 2D failures.

📍 Affects 2 files
  • frontend/src/components/graph/KnowledgeGraph.tsx#L141-L141 (this comment)
  • frontend/src/components/graph/KnowledgeGraph.test.tsx#L30-L50
  • frontend/src/components/graph/KnowledgeGraph.test.tsx#L148-L176
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/graph/KnowledgeGraph.tsx` at line 141, In
frontend/src/components/graph/KnowledgeGraph.tsx:141-141, wrap the fallback
KnowledgeGraph2D rendered by GraphCrashFallback for crashedMode === "3d" in a
nested ErrorBoundary using the inline status placeholder as its fallback,
preventing 2D failures from reaching the root fallback. In
frontend/src/components/graph/KnowledgeGraph.test.tsx:30-50, add the throw2d
mock control; in frontend/src/components/graph/KnowledgeGraph.test.tsx:148-176,
add a regression test covering simultaneous 3D and 2D failures and asserting the
nested placeholder is rendered.

…led toggle, hardened tests
Reworks the crash containment so the persisted mode is never rewritten:
the wrapper derives an EFFECTIVE mode (one gate encoding) from the raw
persisted wish + a module-memoized WebGL2 probe, and a 3D crash re-probes
capability instead of healing localStorage — capability really gone flips
the effective mode (boundary re-keys, 2D mounts fresh); transient crashes
get a static placeholder with the boundary's reset wired to Try again.
The fallback never renders another graph component (a throw during a
boundary's own fallback render is uncatchable by that boundary), and
crashes now log in production.
The toggle swaps native disabled for aria-disabled + aria-describedby so
keyboard/SR users can reach the "3D requires WebGL" reason; the action
stays the accessible name.
Test hardening from the review: crash sentinels throw from a layout
effect (the real #538 crash phase); getContext is stubbed via vi.spyOn so
restoreAllMocks actually restores; storage-key/sync-event constants are
exported and imported by the unit tests (journey mirrors by comment);
SYNC_EVENT gating has direct coverage; the journey anchors its negative
assertions on the new error-fallback testid, re-asserts end state, and
pins the positive path (toggle enabled under real WebGL); the four
divergent next/dynamic passthrough mocks collapse into a shared
hook-safe helper.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Applied all 15 code-review findings in d6cfaea (design rework where the review asked for one, not point patches):

  • Crash containment redesigned (findings 1–5): the persisted mode is now never rewritten. The wrapper derives one EFFECTIVE mode from the raw persisted wish + a module-memoized WebGL2 probe; a 3D crash re-probes capability (the only signal the cached verdict may be stale) — capability really gone flips the effective mode and re-keys the boundary so 2D mounts fresh; transient crashes show a static placeholder with the boundary's reset wired to "Try again". The fallback never renders another graph component (a throw in a boundary's own fallback render is uncatchable by that boundary), and renderer crashes now console.error in production.
  • A11y (11): aria-disabled + aria-describedby replace native disabled; the toggle stays focusable, keeps its action name, and the "3D requires WebGL" reason is reachable.
  • Simplification (9, 10): the gate rule has one render-time encoding (effective = webglOk ? mode : "2d"); the probe is memoized with an explicit force re-probe path.
  • Test hardening (6, 7, 8, 12, 13, 14, 15): crash sentinels throw from a layout effect (the real crash phase); vi.spyOn getContext stub actually restores; exported storage-key/sync-event constants (journey mirrors by comment); direct SYNC_EVENT gating coverage; journey negatives anchor on the new error-fallback testid with an end-state re-assert; a real-browser positive-path test pins the toggle enabled under genuine WebGL2; the four divergent next/dynamic passthrough mocks collapsed into one shared hook-safe helper.

Verification: unit suite 627/627, eslint 0 errors (stale suppressions pruned), tsc clean, full e2e lane 44/44 (both #538 journeys) + oracles clean, run from a fresh worktree via one flock'd up→test→down cycle.

🤖 Generated with Claude Code

@AndresL230
AndresL230 merged commit 5c11b0c into mainAug 13, 2026
6 of 7 checks passed
@AndresL230
AndresL230 deleted the fix/538-graph-webgl-fallback branch August 13, 2026 01:07
Sign up for freeto 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.

Dashboard crashes to the root error fallback on browsers without WebGL when 3D graph mode is persisted

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
fix(graph): gate 3D mode on WebGL2 and contain renderer crashes by AndresL230 · Pull Request #539 · SaplingLearn/Sapling · GitHub
Skip to content

fix(graph): gate 3D mode on WebGL2 and contain renderer crashes - #539

Merged
AndresL230 merged 2 commits into
mainfrom
fix/538-graph-webgl-fallback
Aug 13, 2026
Merged

fix(graph): gate 3D mode on WebGL2 and contain renderer crashes#539
AndresL230 merged 2 commits into
mainfrom
fix/538-graph-webgl-fallback

Conversation

@AndresL230

@AndresL230AndresL230 commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Fixes#538.

The bug

A browser without WebGL that lands on the dashboard (or Tree/Learn) with localStorage["sapling.kg.mode"] = "3d" persisted collapses the entire app into the root error fallback ("We hit a snag"), permanently — "Try again" re-throws. Chain:

  1. The KnowledgeGraph wrapper honoured the persisted "3d" unconditionally — no WebGL capability check existed anywhere in the frontend.
  2. react-force-graph-3d mounts its engine synchronously in a useLayoutEffect (react-kapsule), constructing new three.WebGLRenderer(...).
  3. three.js r163+ throwsError creating WebGL context. from the constructor when canvas.getContext("webgl2") returns null.
  4. The nearest boundary was the root one (app/layout.tsx), so the whole tree unmounted.

A profile that toggled 3D on a WebGL-capable browser was locked out of the app the moment WebGL went away (disabled, GPU blocklist, remote desktop, VM). Fresh profiles were unaffected (mode defaults to 2D) — which is why this read as "used to work".

The fix (both in the wrapper, so Dashboard/Tree/Learn are all covered)

  1. WebGL2 capability gate (webgl2Available()): probe canvas.getContext("webgl2") once per wrapper mount (probe context released via WEBGL_lose_context so probes never count toward the per-page context cap). Without WebGL2:
    • a persisted "3d" is ignored, not rewritten — the preference survives for the profile's WebGL-capable browsers;
    • the mode toggle renders disabled with title/aria-label "3D requires WebGL";
    • the storage/custom-event mode-sync paths and the setter are gated identically.
  2. Graph-local error boundary, keyed by mode: any residual renderer crash (context creation failing despite a successful probe — GPU process crash, context-limit exhaustion) degrades to the 2D graph inline and heals the persisted mode to "2d"; the heal flips the boundary key, clearing the error state into a normal 2D render. A (never-observed) 2D crash renders a minimal placeholder instead of remounting the thing that just threw.

Tests

  • Unit (KnowledgeGraph.test.tsx, 6 tests, written first and watched fail): fresh-profile default pin; 3d-honoured-with-WebGL pin; forces-2D-without-WebGL2; toggle disabled with explanatory label; toggle enabled with WebGL; crash containment + mode heal (RED run reproduced the exact production throw escaping uncaught).
  • Promoted journey (e2e/graph-webgl-fallback.spec.ts): init script nulls the webgl/webgl2 branches of getContext (exactly the API three probes; 2D canvas stays real) and seeds the poisoned "3d" mode → dashboard must render the 2D SVG graph, no root fallback, toggle disabled, preference preserved.

Verification

  • vitest run: 72 files / 624+ tests green (against a jsdom that is itself a no-WebGL environment, so every existing screen test now exercises the gated path).
  • tsc --noEmit clean; eslint clean on changed files (no new suppressions).
  • next build clean.
  • Pre-fix RED run (stack cycle 1): with the wrapper fix stashed and the real production build served, the new journey fails — the dashboard collapses into the root fallback exactly as reported. The journey failed at the "We hit a snag" count-0 assertion (exit 1) — the root fallback was live on the page.
  • Post-fix full lane (stack cycle 2): entire Chapter-1 Playwright lane + deterministic oracles green. 43 passed (1.5m), oracles exit 0.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved graph rendering when WebGL2 is unavailable or 3D rendering fails.
    • Automatically falls back to the 2D graph while preserving the saved mode preference.
    • Disabled the 3D toggle when unsupported, with an explanatory message.
    • Added recovery and retry handling for temporary graph rendering failures.
    • Prevented graph errors from disrupting the rest of the dashboard.
  • Tests

    • Added coverage for WebGL detection, fallback behavior, mode synchronization, toggle states, and renderer recovery.
    • Added end-to-end coverage for 2D fallback and supported 3D environments.

A browser without WebGL landing on any graph surface with a persisted
"3d" mode collapsed the whole app into the root error fallback: the
wrapper honoured localStorage unconditionally, three r163+ throws from
the WebGLRenderer constructor in a mount-time layout effect, and the
only boundary above it was the root one.
The wrapper now probes canvas.getContext("webgl2") before honouring a
persisted "3d" (ignoring, not rewriting, the preference), disables the
mode toggle with "3D requires WebGL" when the probe fails, and wraps
the graph slot in a mode-keyed local ErrorBoundary whose fallback
degrades a crashed 3D renderer to the 2D graph and heals the persisted
mode to "2d".
Unit tests were written first and watched fail (the crash test
reproduced the exact production throw escaping uncaught); the promoted
journey simulates a no-WebGL browser via an init-script getContext
override and failed against a pre-fix stack build with the dashboard on
the root fallback, exactly as reported.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 12, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-stagingd6cfaeaCommit Preview URL

Branch Preview URL
Aug 13 2026, 01:07 AM

@supabase

supabaseBot commented Aug 12, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: feb9f6c1-33ca-40db-986b-72181daea902

📥 Commits

Reviewing files that changed from the base of the PR and between ce7c291 and d6cfaea.

📒 Files selected for processing (11)
  • docs/frontend-testids.md
  • frontend/e2e/graph-webgl-fallback.spec.ts
  • frontend/eslint-suppressions.json
  • frontend/src/components/ErrorBoundary.tsx
  • frontend/src/components/graph/KnowledgeGraph.test.tsx
  • frontend/src/components/graph/KnowledgeGraph.tsx
  • frontend/src/components/graph/KnowledgeGraph3D.test.tsx
  • frontend/src/components/graph/KnowledgeGraph3D.testmode.test.tsx
  • frontend/src/components/graph/KnowledgeGraph3D.tsx
  • frontend/src/components/screens/AdminAnalytics.test.tsx
  • frontend/src/test-utils/mockNextDynamic.tsx

📝 Walkthrough

Walkthrough

KnowledgeGraph now detects WebGL2 before selecting 3D mode, contains renderer failures locally, preserves the stored preference, and adds unit and Playwright regression coverage.

Changes

Knowledge graph WebGL fallback

Layer / File(s)Summary
WebGL2 capability and mode gating
frontend/src/components/graph/KnowledgeGraph.tsx, frontend/src/components/graph/KnowledgeGraph3D.tsx
The graph probes WebGL2, derives an effective mode without changing the stored preference, and disables the 3D toggle when WebGL2 is unavailable.
Local renderer failure recovery
frontend/src/components/graph/KnowledgeGraph.tsx, frontend/src/components/ErrorBoundary.tsx, docs/frontend-testids.md
A graph-local boundary shows retryable fallback UI, re-probes after 3D failures, and switches to 2D when capability is lost.
Regression coverage and test infrastructure
frontend/src/components/graph/KnowledgeGraph.test.tsx, frontend/e2e/graph-webgl-fallback.spec.ts, frontend/src/test-utils/mockNextDynamic.tsx, frontend/src/components/graph/*test.tsx, frontend/src/components/screens/AdminAnalytics.test.tsx
Tests cover mode selection, WebGL2 gating, synchronization, toggle accessibility, crash recovery, retries, and persisted preferences. Shared dynamic-import mocking replaces local implementations.

Estimated code review effort: 4 (Complex) | ~45 minutes

Mergeability Score:⚪ Minimal · up to d6cfa

The change gates 3D rendering on WebGL2 and falls back to the 2D graph when renderer creation fails; no actionable merge-blocking risk remains after normal checks and review.

Sequence Diagram(s)

sequenceDiagram
participant Browser
participant KnowledgeGraph
participant localStorage
participant KnowledgeGraph3D
participant KnowledgeGraph2D
Browser->>KnowledgeGraph: mount graph
KnowledgeGraph->>Browser: probe WebGL2
Browser-->>KnowledgeGraph: return capability result
KnowledgeGraph->>localStorage: read persisted mode
KnowledgeGraph->>KnowledgeGraph2D: render effective 2D mode when WebGL2 is unavailable
KnowledgeGraph-->>Browser: expose disabled 3D toggle explanation
KnowledgeGraph->>KnowledgeGraph3D: mount 3D renderer when supported
KnowledgeGraph3D-->>KnowledgeGraph: report renderer failure
KnowledgeGraph->>KnowledgeGraph2D: render local 2D fallback
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 25.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly and concisely identifies the primary WebGL2 gating and renderer crash-containment changes.
Description check✅ PassedThe description explains the bug, implementation, linked issue, tests, and verification, although it does not follow every template heading.
Linked Issues check✅ PassedThe changes satisfy [#538] by gating persisted 3D mode, preserving preferences, containing graph crashes, and adding regression tests.
Out of Scope Changes check✅ PassedAll changes support the WebGL2 fallback, graph crash containment, accessibility, documentation, or related test infrastructure objectives.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/538-graph-webgl-fallback

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/src/components/graph/KnowledgeGraph.tsx`:
- Line 141: In frontend/src/components/graph/KnowledgeGraph.tsx:141-141, wrap
the fallback KnowledgeGraph2D rendered by GraphCrashFallback for crashedMode ===
"3d" in a nested ErrorBoundary using the inline status placeholder as its
fallback, preventing 2D failures from reaching the root fallback. In
frontend/src/components/graph/KnowledgeGraph.test.tsx:30-50, add the throw2d
mock control; in frontend/src/components/graph/KnowledgeGraph.test.tsx:148-176,
add a regression test covering simultaneous 3D and 2D failures and asserting the
nested placeholder is rendered.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5c77b3cc-2ebe-41aa-af76-de2ad1793ce2

📥 Commits

Reviewing files that changed from the base of the PR and between 7681c48 and ce7c291.

📒 Files selected for processing (3)
  • frontend/e2e/graph-webgl-fallback.spec.ts
  • frontend/src/components/graph/KnowledgeGraph.test.tsx
  • frontend/src/components/graph/KnowledgeGraph.tsx

React.useEffect(() => {
if (crashedMode === "3d") heal("2d");
}, [crashedMode, heal]);
if (crashedMode === "3d") return <KnowledgeGraph2D {...graphProps} />;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -e
printf'%s\n''--- KnowledgeGraph.tsx outline ---'
ast-grep outline frontend/src/components/graph/KnowledgeGraph.tsx
printf'%s\n''--- Relevant implementation ---'
sed -n '1,230p' frontend/src/components/graph/KnowledgeGraph.tsx
printf'%s\n''--- Relevant test structure ---'
sed -n '1,230p' frontend/src/components/graph/KnowledgeGraph.test.tsx

Repository: SaplingLearn/Sapling

Length of output: 14232


🏁 Script executed:

#!/bin/bashset -e
printf'%s\n''--- ErrorBoundary definition and usages ---'
fd -i 'ErrorBoundary' frontend/src
rg -n -C 12 'class ErrorBoundary|function ErrorBoundary|<ErrorBoundary' frontend/src
printf'%s\n''--- GraphCrashFallback references ---'
rg -n -C 8 'GraphCrashFallback|crashedMode === "3d"|The graph couldn' frontend/src/components/graph

Repository: SaplingLearn/Sapling

Length of output: 10536


🏁 Script executed:

#!/bin/bashset -e
cat -n frontend/src/components/ErrorBoundary.tsx
printf'%s\n''--- package and test scripts ---'if [ -f frontend/package.json ];then
sed -n '1,220p' frontend/package.json
fiif [ -f package.json ];then
sed -n '1,180p' package.json
fi

Repository: SaplingLearn/Sapling

Length of output: 7291


🏁 Script executed:

#!/bin/bashset -e
python3 - <<'PY'from pathlib import Pathimpl = Path("frontend/src/components/graph/KnowledgeGraph.tsx").read_text()boundary = Path("frontend/src/components/ErrorBoundary.tsx").read_text()test = Path("frontend/src/components/graph/KnowledgeGraph.test.tsx").read_text()checks = { "outer boundary invokes GraphCrashFallback as its fallback": ( '<GraphCrashFallback crashedMode={mode} graphProps={props} heal={setMode} />' in impl ), "3D fallback directly renders 2D": ( 'if (crashedMode === "3d") return <KnowledgeGraph2D {...graphProps} />;' in impl ), "ErrorBoundary calls fallback from its own render": ( "if (this.props.fallback) return this.props.fallback(this.state.error, this.reset);" in boundary ), "2D mock has no throw2d control": ( "throw2d" not in test ), "existing crash test only arms 3D": ( "mockState.throw3d = true;" in test and "mockState.throw2d" not in test ),}for name, result in checks.items(): print(f"{'PASS' if result else 'FAIL'}: {name}")# This models the relevant React error-boundary rule:# an error thrown while an ErrorBoundary renders its own fallback# cannot be handled by that same boundary.print("MODEL: 3D child throws -> outer boundary renders GraphCrashFallback")print("MODEL: GraphCrashFallback renders 2D directly -> a 2D throw occurs in outer fallback render")print("MODEL: without a nested boundary, that throw propagates to the next ancestor boundary")PY

Repository: SaplingLearn/Sapling

Length of output: 637


Contain a 2D failure during the 3D fallback.

When KnowledgeGraph3D throws, GraphCrashFallback renders KnowledgeGraph2D from the outer ErrorBoundary fallback. A 2D error then propagates to the root fallback. Wrap the fallback 2D graph in a nested ErrorBoundary with the inline status placeholder as its fallback. Add a throw2d mock control and regression test for simultaneous 3D and 2D failures.

📍 Affects 2 files
  • frontend/src/components/graph/KnowledgeGraph.tsx#L141-L141 (this comment)
  • frontend/src/components/graph/KnowledgeGraph.test.tsx#L30-L50
  • frontend/src/components/graph/KnowledgeGraph.test.tsx#L148-L176
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/graph/KnowledgeGraph.tsx` at line 141, In
frontend/src/components/graph/KnowledgeGraph.tsx:141-141, wrap the fallback
KnowledgeGraph2D rendered by GraphCrashFallback for crashedMode === "3d" in a
nested ErrorBoundary using the inline status placeholder as its fallback,
preventing 2D failures from reaching the root fallback. In
frontend/src/components/graph/KnowledgeGraph.test.tsx:30-50, add the throw2d
mock control; in frontend/src/components/graph/KnowledgeGraph.test.tsx:148-176,
add a regression test covering simultaneous 3D and 2D failures and asserting the
nested placeholder is rendered.

…led toggle, hardened tests
Reworks the crash containment so the persisted mode is never rewritten:
the wrapper derives an EFFECTIVE mode (one gate encoding) from the raw
persisted wish + a module-memoized WebGL2 probe, and a 3D crash re-probes
capability instead of healing localStorage — capability really gone flips
the effective mode (boundary re-keys, 2D mounts fresh); transient crashes
get a static placeholder with the boundary's reset wired to Try again.
The fallback never renders another graph component (a throw during a
boundary's own fallback render is uncatchable by that boundary), and
crashes now log in production.
The toggle swaps native disabled for aria-disabled + aria-describedby so
keyboard/SR users can reach the "3D requires WebGL" reason; the action
stays the accessible name.
Test hardening from the review: crash sentinels throw from a layout
effect (the real #538 crash phase); getContext is stubbed via vi.spyOn so
restoreAllMocks actually restores; storage-key/sync-event constants are
exported and imported by the unit tests (journey mirrors by comment);
SYNC_EVENT gating has direct coverage; the journey anchors its negative
assertions on the new error-fallback testid, re-asserts end state, and
pins the positive path (toggle enabled under real WebGL); the four
divergent next/dynamic passthrough mocks collapse into a shared
hook-safe helper.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Applied all 15 code-review findings in d6cfaea (design rework where the review asked for one, not point patches):

  • Crash containment redesigned (findings 1–5): the persisted mode is now never rewritten. The wrapper derives one EFFECTIVE mode from the raw persisted wish + a module-memoized WebGL2 probe; a 3D crash re-probes capability (the only signal the cached verdict may be stale) — capability really gone flips the effective mode and re-keys the boundary so 2D mounts fresh; transient crashes show a static placeholder with the boundary's reset wired to "Try again". The fallback never renders another graph component (a throw in a boundary's own fallback render is uncatchable by that boundary), and renderer crashes now console.error in production.
  • A11y (11): aria-disabled + aria-describedby replace native disabled; the toggle stays focusable, keeps its action name, and the "3D requires WebGL" reason is reachable.
  • Simplification (9, 10): the gate rule has one render-time encoding (effective = webglOk ? mode : "2d"); the probe is memoized with an explicit force re-probe path.
  • Test hardening (6, 7, 8, 12, 13, 14, 15): crash sentinels throw from a layout effect (the real crash phase); vi.spyOn getContext stub actually restores; exported storage-key/sync-event constants (journey mirrors by comment); direct SYNC_EVENT gating coverage; journey negatives anchor on the new error-fallback testid with an end-state re-assert; a real-browser positive-path test pins the toggle enabled under genuine WebGL2; the four divergent next/dynamic passthrough mocks collapsed into one shared hook-safe helper.

Verification: unit suite 627/627, eslint 0 errors (stale suppressions pruned), tsc clean, full e2e lane 44/44 (both #538 journeys) + oracles clean, run from a fresh worktree via one flock'd up→test→down cycle.

🤖 Generated with Claude Code

@AndresL230
AndresL230 merged commit 5c11b0c into mainAug 13, 2026
6 of 7 checks passed
@AndresL230
AndresL230 deleted the fix/538-graph-webgl-fallback branch August 13, 2026 01:07
Sign up for freeto 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.

Dashboard crashes to the root error fallback on browsers without WebGL when 3D graph mode is persisted

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(graph): gate 3D mode on WebGL2 and contain renderer crashes by AndresL230 · Pull Request #539 · SaplingLearn/Sapling · GitHub
Skip to content

fix(graph): gate 3D mode on WebGL2 and contain renderer crashes - #539

Merged
AndresL230 merged 2 commits into
mainfrom
fix/538-graph-webgl-fallback
Aug 13, 2026
Merged

fix(graph): gate 3D mode on WebGL2 and contain renderer crashes#539
AndresL230 merged 2 commits into
mainfrom
fix/538-graph-webgl-fallback

Conversation

@AndresL230

@AndresL230AndresL230 commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Fixes#538.

The bug

A browser without WebGL that lands on the dashboard (or Tree/Learn) with localStorage["sapling.kg.mode"] = "3d" persisted collapses the entire app into the root error fallback ("We hit a snag"), permanently — "Try again" re-throws. Chain:

  1. The KnowledgeGraph wrapper honoured the persisted "3d" unconditionally — no WebGL capability check existed anywhere in the frontend.
  2. react-force-graph-3d mounts its engine synchronously in a useLayoutEffect (react-kapsule), constructing new three.WebGLRenderer(...).
  3. three.js r163+ throwsError creating WebGL context. from the constructor when canvas.getContext("webgl2") returns null.
  4. The nearest boundary was the root one (app/layout.tsx), so the whole tree unmounted.

A profile that toggled 3D on a WebGL-capable browser was locked out of the app the moment WebGL went away (disabled, GPU blocklist, remote desktop, VM). Fresh profiles were unaffected (mode defaults to 2D) — which is why this read as "used to work".

The fix (both in the wrapper, so Dashboard/Tree/Learn are all covered)

  1. WebGL2 capability gate (webgl2Available()): probe canvas.getContext("webgl2") once per wrapper mount (probe context released via WEBGL_lose_context so probes never count toward the per-page context cap). Without WebGL2:
    • a persisted "3d" is ignored, not rewritten — the preference survives for the profile's WebGL-capable browsers;
    • the mode toggle renders disabled with title/aria-label "3D requires WebGL";
    • the storage/custom-event mode-sync paths and the setter are gated identically.
  2. Graph-local error boundary, keyed by mode: any residual renderer crash (context creation failing despite a successful probe — GPU process crash, context-limit exhaustion) degrades to the 2D graph inline and heals the persisted mode to "2d"; the heal flips the boundary key, clearing the error state into a normal 2D render. A (never-observed) 2D crash renders a minimal placeholder instead of remounting the thing that just threw.

Tests

  • Unit (KnowledgeGraph.test.tsx, 6 tests, written first and watched fail): fresh-profile default pin; 3d-honoured-with-WebGL pin; forces-2D-without-WebGL2; toggle disabled with explanatory label; toggle enabled with WebGL; crash containment + mode heal (RED run reproduced the exact production throw escaping uncaught).
  • Promoted journey (e2e/graph-webgl-fallback.spec.ts): init script nulls the webgl/webgl2 branches of getContext (exactly the API three probes; 2D canvas stays real) and seeds the poisoned "3d" mode → dashboard must render the 2D SVG graph, no root fallback, toggle disabled, preference preserved.

Verification

  • vitest run: 72 files / 624+ tests green (against a jsdom that is itself a no-WebGL environment, so every existing screen test now exercises the gated path).
  • tsc --noEmit clean; eslint clean on changed files (no new suppressions).
  • next build clean.
  • Pre-fix RED run (stack cycle 1): with the wrapper fix stashed and the real production build served, the new journey fails — the dashboard collapses into the root fallback exactly as reported. The journey failed at the "We hit a snag" count-0 assertion (exit 1) — the root fallback was live on the page.
  • Post-fix full lane (stack cycle 2): entire Chapter-1 Playwright lane + deterministic oracles green. 43 passed (1.5m), oracles exit 0.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved graph rendering when WebGL2 is unavailable or 3D rendering fails.
    • Automatically falls back to the 2D graph while preserving the saved mode preference.
    • Disabled the 3D toggle when unsupported, with an explanatory message.
    • Added recovery and retry handling for temporary graph rendering failures.
    • Prevented graph errors from disrupting the rest of the dashboard.
  • Tests

    • Added coverage for WebGL detection, fallback behavior, mode synchronization, toggle states, and renderer recovery.
    • Added end-to-end coverage for 2D fallback and supported 3D environments.

A browser without WebGL landing on any graph surface with a persisted
"3d" mode collapsed the whole app into the root error fallback: the
wrapper honoured localStorage unconditionally, three r163+ throws from
the WebGLRenderer constructor in a mount-time layout effect, and the
only boundary above it was the root one.
The wrapper now probes canvas.getContext("webgl2") before honouring a
persisted "3d" (ignoring, not rewriting, the preference), disables the
mode toggle with "3D requires WebGL" when the probe fails, and wraps
the graph slot in a mode-keyed local ErrorBoundary whose fallback
degrades a crashed 3D renderer to the 2D graph and heals the persisted
mode to "2d".
Unit tests were written first and watched fail (the crash test
reproduced the exact production throw escaping uncaught); the promoted
journey simulates a no-WebGL browser via an init-script getContext
override and failed against a pre-fix stack build with the dashboard on
the root fallback, exactly as reported.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 12, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-stagingd6cfaeaCommit Preview URL

Branch Preview URL
Aug 13 2026, 01:07 AM

@supabase

supabaseBot commented Aug 12, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: feb9f6c1-33ca-40db-986b-72181daea902

📥 Commits

Reviewing files that changed from the base of the PR and between ce7c291 and d6cfaea.

📒 Files selected for processing (11)
  • docs/frontend-testids.md
  • frontend/e2e/graph-webgl-fallback.spec.ts
  • frontend/eslint-suppressions.json
  • frontend/src/components/ErrorBoundary.tsx
  • frontend/src/components/graph/KnowledgeGraph.test.tsx
  • frontend/src/components/graph/KnowledgeGraph.tsx
  • frontend/src/components/graph/KnowledgeGraph3D.test.tsx
  • frontend/src/components/graph/KnowledgeGraph3D.testmode.test.tsx
  • frontend/src/components/graph/KnowledgeGraph3D.tsx
  • frontend/src/components/screens/AdminAnalytics.test.tsx
  • frontend/src/test-utils/mockNextDynamic.tsx

📝 Walkthrough

Walkthrough

KnowledgeGraph now detects WebGL2 before selecting 3D mode, contains renderer failures locally, preserves the stored preference, and adds unit and Playwright regression coverage.

Changes

Knowledge graph WebGL fallback

Layer / File(s)Summary
WebGL2 capability and mode gating
frontend/src/components/graph/KnowledgeGraph.tsx, frontend/src/components/graph/KnowledgeGraph3D.tsx
The graph probes WebGL2, derives an effective mode without changing the stored preference, and disables the 3D toggle when WebGL2 is unavailable.
Local renderer failure recovery
frontend/src/components/graph/KnowledgeGraph.tsx, frontend/src/components/ErrorBoundary.tsx, docs/frontend-testids.md
A graph-local boundary shows retryable fallback UI, re-probes after 3D failures, and switches to 2D when capability is lost.
Regression coverage and test infrastructure
frontend/src/components/graph/KnowledgeGraph.test.tsx, frontend/e2e/graph-webgl-fallback.spec.ts, frontend/src/test-utils/mockNextDynamic.tsx, frontend/src/components/graph/*test.tsx, frontend/src/components/screens/AdminAnalytics.test.tsx
Tests cover mode selection, WebGL2 gating, synchronization, toggle accessibility, crash recovery, retries, and persisted preferences. Shared dynamic-import mocking replaces local implementations.

Estimated code review effort: 4 (Complex) | ~45 minutes

Mergeability Score:⚪ Minimal · up to d6cfa

The change gates 3D rendering on WebGL2 and falls back to the 2D graph when renderer creation fails; no actionable merge-blocking risk remains after normal checks and review.

Sequence Diagram(s)

sequenceDiagram
participant Browser
participant KnowledgeGraph
participant localStorage
participant KnowledgeGraph3D
participant KnowledgeGraph2D
Browser->>KnowledgeGraph: mount graph
KnowledgeGraph->>Browser: probe WebGL2
Browser-->>KnowledgeGraph: return capability result
KnowledgeGraph->>localStorage: read persisted mode
KnowledgeGraph->>KnowledgeGraph2D: render effective 2D mode when WebGL2 is unavailable
KnowledgeGraph-->>Browser: expose disabled 3D toggle explanation
KnowledgeGraph->>KnowledgeGraph3D: mount 3D renderer when supported
KnowledgeGraph3D-->>KnowledgeGraph: report renderer failure
KnowledgeGraph->>KnowledgeGraph2D: render local 2D fallback
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 25.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly and concisely identifies the primary WebGL2 gating and renderer crash-containment changes.
Description check✅ PassedThe description explains the bug, implementation, linked issue, tests, and verification, although it does not follow every template heading.
Linked Issues check✅ PassedThe changes satisfy [#538] by gating persisted 3D mode, preserving preferences, containing graph crashes, and adding regression tests.
Out of Scope Changes check✅ PassedAll changes support the WebGL2 fallback, graph crash containment, accessibility, documentation, or related test infrastructure objectives.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/538-graph-webgl-fallback

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/src/components/graph/KnowledgeGraph.tsx`:
- Line 141: In frontend/src/components/graph/KnowledgeGraph.tsx:141-141, wrap
the fallback KnowledgeGraph2D rendered by GraphCrashFallback for crashedMode ===
"3d" in a nested ErrorBoundary using the inline status placeholder as its
fallback, preventing 2D failures from reaching the root fallback. In
frontend/src/components/graph/KnowledgeGraph.test.tsx:30-50, add the throw2d
mock control; in frontend/src/components/graph/KnowledgeGraph.test.tsx:148-176,
add a regression test covering simultaneous 3D and 2D failures and asserting the
nested placeholder is rendered.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5c77b3cc-2ebe-41aa-af76-de2ad1793ce2

📥 Commits

Reviewing files that changed from the base of the PR and between 7681c48 and ce7c291.

📒 Files selected for processing (3)
  • frontend/e2e/graph-webgl-fallback.spec.ts
  • frontend/src/components/graph/KnowledgeGraph.test.tsx
  • frontend/src/components/graph/KnowledgeGraph.tsx

React.useEffect(() => {
if (crashedMode === "3d") heal("2d");
}, [crashedMode, heal]);
if (crashedMode === "3d") return <KnowledgeGraph2D {...graphProps} />;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -e
printf'%s\n''--- KnowledgeGraph.tsx outline ---'
ast-grep outline frontend/src/components/graph/KnowledgeGraph.tsx
printf'%s\n''--- Relevant implementation ---'
sed -n '1,230p' frontend/src/components/graph/KnowledgeGraph.tsx
printf'%s\n''--- Relevant test structure ---'
sed -n '1,230p' frontend/src/components/graph/KnowledgeGraph.test.tsx

Repository: SaplingLearn/Sapling

Length of output: 14232


🏁 Script executed:

#!/bin/bashset -e
printf'%s\n''--- ErrorBoundary definition and usages ---'
fd -i 'ErrorBoundary' frontend/src
rg -n -C 12 'class ErrorBoundary|function ErrorBoundary|<ErrorBoundary' frontend/src
printf'%s\n''--- GraphCrashFallback references ---'
rg -n -C 8 'GraphCrashFallback|crashedMode === "3d"|The graph couldn' frontend/src/components/graph

Repository: SaplingLearn/Sapling

Length of output: 10536


🏁 Script executed:

#!/bin/bashset -e
cat -n frontend/src/components/ErrorBoundary.tsx
printf'%s\n''--- package and test scripts ---'if [ -f frontend/package.json ];then
sed -n '1,220p' frontend/package.json
fiif [ -f package.json ];then
sed -n '1,180p' package.json
fi

Repository: SaplingLearn/Sapling

Length of output: 7291


🏁 Script executed:

#!/bin/bashset -e
python3 - <<'PY'from pathlib import Pathimpl = Path("frontend/src/components/graph/KnowledgeGraph.tsx").read_text()boundary = Path("frontend/src/components/ErrorBoundary.tsx").read_text()test = Path("frontend/src/components/graph/KnowledgeGraph.test.tsx").read_text()checks = { "outer boundary invokes GraphCrashFallback as its fallback": ( '<GraphCrashFallback crashedMode={mode} graphProps={props} heal={setMode} />' in impl ), "3D fallback directly renders 2D": ( 'if (crashedMode === "3d") return <KnowledgeGraph2D {...graphProps} />;' in impl ), "ErrorBoundary calls fallback from its own render": ( "if (this.props.fallback) return this.props.fallback(this.state.error, this.reset);" in boundary ), "2D mock has no throw2d control": ( "throw2d" not in test ), "existing crash test only arms 3D": ( "mockState.throw3d = true;" in test and "mockState.throw2d" not in test ),}for name, result in checks.items(): print(f"{'PASS' if result else 'FAIL'}: {name}")# This models the relevant React error-boundary rule:# an error thrown while an ErrorBoundary renders its own fallback# cannot be handled by that same boundary.print("MODEL: 3D child throws -> outer boundary renders GraphCrashFallback")print("MODEL: GraphCrashFallback renders 2D directly -> a 2D throw occurs in outer fallback render")print("MODEL: without a nested boundary, that throw propagates to the next ancestor boundary")PY

Repository: SaplingLearn/Sapling

Length of output: 637


Contain a 2D failure during the 3D fallback.

When KnowledgeGraph3D throws, GraphCrashFallback renders KnowledgeGraph2D from the outer ErrorBoundary fallback. A 2D error then propagates to the root fallback. Wrap the fallback 2D graph in a nested ErrorBoundary with the inline status placeholder as its fallback. Add a throw2d mock control and regression test for simultaneous 3D and 2D failures.

📍 Affects 2 files
  • frontend/src/components/graph/KnowledgeGraph.tsx#L141-L141 (this comment)
  • frontend/src/components/graph/KnowledgeGraph.test.tsx#L30-L50
  • frontend/src/components/graph/KnowledgeGraph.test.tsx#L148-L176
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/graph/KnowledgeGraph.tsx` at line 141, In
frontend/src/components/graph/KnowledgeGraph.tsx:141-141, wrap the fallback
KnowledgeGraph2D rendered by GraphCrashFallback for crashedMode === "3d" in a
nested ErrorBoundary using the inline status placeholder as its fallback,
preventing 2D failures from reaching the root fallback. In
frontend/src/components/graph/KnowledgeGraph.test.tsx:30-50, add the throw2d
mock control; in frontend/src/components/graph/KnowledgeGraph.test.tsx:148-176,
add a regression test covering simultaneous 3D and 2D failures and asserting the
nested placeholder is rendered.

…led toggle, hardened tests
Reworks the crash containment so the persisted mode is never rewritten:
the wrapper derives an EFFECTIVE mode (one gate encoding) from the raw
persisted wish + a module-memoized WebGL2 probe, and a 3D crash re-probes
capability instead of healing localStorage — capability really gone flips
the effective mode (boundary re-keys, 2D mounts fresh); transient crashes
get a static placeholder with the boundary's reset wired to Try again.
The fallback never renders another graph component (a throw during a
boundary's own fallback render is uncatchable by that boundary), and
crashes now log in production.
The toggle swaps native disabled for aria-disabled + aria-describedby so
keyboard/SR users can reach the "3D requires WebGL" reason; the action
stays the accessible name.
Test hardening from the review: crash sentinels throw from a layout
effect (the real #538 crash phase); getContext is stubbed via vi.spyOn so
restoreAllMocks actually restores; storage-key/sync-event constants are
exported and imported by the unit tests (journey mirrors by comment);
SYNC_EVENT gating has direct coverage; the journey anchors its negative
assertions on the new error-fallback testid, re-asserts end state, and
pins the positive path (toggle enabled under real WebGL); the four
divergent next/dynamic passthrough mocks collapse into a shared
hook-safe helper.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Applied all 15 code-review findings in d6cfaea (design rework where the review asked for one, not point patches):

  • Crash containment redesigned (findings 1–5): the persisted mode is now never rewritten. The wrapper derives one EFFECTIVE mode from the raw persisted wish + a module-memoized WebGL2 probe; a 3D crash re-probes capability (the only signal the cached verdict may be stale) — capability really gone flips the effective mode and re-keys the boundary so 2D mounts fresh; transient crashes show a static placeholder with the boundary's reset wired to "Try again". The fallback never renders another graph component (a throw in a boundary's own fallback render is uncatchable by that boundary), and renderer crashes now console.error in production.
  • A11y (11): aria-disabled + aria-describedby replace native disabled; the toggle stays focusable, keeps its action name, and the "3D requires WebGL" reason is reachable.
  • Simplification (9, 10): the gate rule has one render-time encoding (effective = webglOk ? mode : "2d"); the probe is memoized with an explicit force re-probe path.
  • Test hardening (6, 7, 8, 12, 13, 14, 15): crash sentinels throw from a layout effect (the real crash phase); vi.spyOn getContext stub actually restores; exported storage-key/sync-event constants (journey mirrors by comment); direct SYNC_EVENT gating coverage; journey negatives anchor on the new error-fallback testid with an end-state re-assert; a real-browser positive-path test pins the toggle enabled under genuine WebGL2; the four divergent next/dynamic passthrough mocks collapsed into one shared hook-safe helper.

Verification: unit suite 627/627, eslint 0 errors (stale suppressions pruned), tsc clean, full e2e lane 44/44 (both #538 journeys) + oracles clean, run from a fresh worktree via one flock'd up→test→down cycle.

🤖 Generated with Claude Code

@AndresL230
AndresL230 merged commit 5c11b0c into mainAug 13, 2026
6 of 7 checks passed
@AndresL230
AndresL230 deleted the fix/538-graph-webgl-fallback branch August 13, 2026 01:07
Sign up for freeto 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.

Dashboard crashes to the root error fallback on browsers without WebGL when 3D graph mode is persisted

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(graph): gate 3D mode on WebGL2 and contain renderer crashes by AndresL230 · Pull Request #539 · SaplingLearn/Sapling · GitHub
Skip to content

fix(graph): gate 3D mode on WebGL2 and contain renderer crashes - #539

Merged
AndresL230 merged 2 commits into
mainfrom
fix/538-graph-webgl-fallback
Aug 13, 2026
Merged

fix(graph): gate 3D mode on WebGL2 and contain renderer crashes#539
AndresL230 merged 2 commits into
mainfrom
fix/538-graph-webgl-fallback

Conversation

@AndresL230

@AndresL230AndresL230 commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Fixes#538.

The bug

A browser without WebGL that lands on the dashboard (or Tree/Learn) with localStorage["sapling.kg.mode"] = "3d" persisted collapses the entire app into the root error fallback ("We hit a snag"), permanently — "Try again" re-throws. Chain:

  1. The KnowledgeGraph wrapper honoured the persisted "3d" unconditionally — no WebGL capability check existed anywhere in the frontend.
  2. react-force-graph-3d mounts its engine synchronously in a useLayoutEffect (react-kapsule), constructing new three.WebGLRenderer(...).
  3. three.js r163+ throwsError creating WebGL context. from the constructor when canvas.getContext("webgl2") returns null.
  4. The nearest boundary was the root one (app/layout.tsx), so the whole tree unmounted.

A profile that toggled 3D on a WebGL-capable browser was locked out of the app the moment WebGL went away (disabled, GPU blocklist, remote desktop, VM). Fresh profiles were unaffected (mode defaults to 2D) — which is why this read as "used to work".

The fix (both in the wrapper, so Dashboard/Tree/Learn are all covered)

  1. WebGL2 capability gate (webgl2Available()): probe canvas.getContext("webgl2") once per wrapper mount (probe context released via WEBGL_lose_context so probes never count toward the per-page context cap). Without WebGL2:
    • a persisted "3d" is ignored, not rewritten — the preference survives for the profile's WebGL-capable browsers;
    • the mode toggle renders disabled with title/aria-label "3D requires WebGL";
    • the storage/custom-event mode-sync paths and the setter are gated identically.
  2. Graph-local error boundary, keyed by mode: any residual renderer crash (context creation failing despite a successful probe — GPU process crash, context-limit exhaustion) degrades to the 2D graph inline and heals the persisted mode to "2d"; the heal flips the boundary key, clearing the error state into a normal 2D render. A (never-observed) 2D crash renders a minimal placeholder instead of remounting the thing that just threw.

Tests

  • Unit (KnowledgeGraph.test.tsx, 6 tests, written first and watched fail): fresh-profile default pin; 3d-honoured-with-WebGL pin; forces-2D-without-WebGL2; toggle disabled with explanatory label; toggle enabled with WebGL; crash containment + mode heal (RED run reproduced the exact production throw escaping uncaught).
  • Promoted journey (e2e/graph-webgl-fallback.spec.ts): init script nulls the webgl/webgl2 branches of getContext (exactly the API three probes; 2D canvas stays real) and seeds the poisoned "3d" mode → dashboard must render the 2D SVG graph, no root fallback, toggle disabled, preference preserved.

Verification

  • vitest run: 72 files / 624+ tests green (against a jsdom that is itself a no-WebGL environment, so every existing screen test now exercises the gated path).
  • tsc --noEmit clean; eslint clean on changed files (no new suppressions).
  • next build clean.
  • Pre-fix RED run (stack cycle 1): with the wrapper fix stashed and the real production build served, the new journey fails — the dashboard collapses into the root fallback exactly as reported. The journey failed at the "We hit a snag" count-0 assertion (exit 1) — the root fallback was live on the page.
  • Post-fix full lane (stack cycle 2): entire Chapter-1 Playwright lane + deterministic oracles green. 43 passed (1.5m), oracles exit 0.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved graph rendering when WebGL2 is unavailable or 3D rendering fails.
    • Automatically falls back to the 2D graph while preserving the saved mode preference.
    • Disabled the 3D toggle when unsupported, with an explanatory message.
    • Added recovery and retry handling for temporary graph rendering failures.
    • Prevented graph errors from disrupting the rest of the dashboard.
  • Tests

    • Added coverage for WebGL detection, fallback behavior, mode synchronization, toggle states, and renderer recovery.
    • Added end-to-end coverage for 2D fallback and supported 3D environments.

A browser without WebGL landing on any graph surface with a persisted
"3d" mode collapsed the whole app into the root error fallback: the
wrapper honoured localStorage unconditionally, three r163+ throws from
the WebGLRenderer constructor in a mount-time layout effect, and the
only boundary above it was the root one.
The wrapper now probes canvas.getContext("webgl2") before honouring a
persisted "3d" (ignoring, not rewriting, the preference), disables the
mode toggle with "3D requires WebGL" when the probe fails, and wraps
the graph slot in a mode-keyed local ErrorBoundary whose fallback
degrades a crashed 3D renderer to the 2D graph and heals the persisted
mode to "2d".
Unit tests were written first and watched fail (the crash test
reproduced the exact production throw escaping uncaught); the promoted
journey simulates a no-WebGL browser via an init-script getContext
override and failed against a pre-fix stack build with the dashboard on
the root fallback, exactly as reported.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 12, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-stagingd6cfaeaCommit Preview URL

Branch Preview URL
Aug 13 2026, 01:07 AM

@supabase

supabaseBot commented Aug 12, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: feb9f6c1-33ca-40db-986b-72181daea902

📥 Commits

Reviewing files that changed from the base of the PR and between ce7c291 and d6cfaea.

📒 Files selected for processing (11)
  • docs/frontend-testids.md
  • frontend/e2e/graph-webgl-fallback.spec.ts
  • frontend/eslint-suppressions.json
  • frontend/src/components/ErrorBoundary.tsx
  • frontend/src/components/graph/KnowledgeGraph.test.tsx
  • frontend/src/components/graph/KnowledgeGraph.tsx
  • frontend/src/components/graph/KnowledgeGraph3D.test.tsx
  • frontend/src/components/graph/KnowledgeGraph3D.testmode.test.tsx
  • frontend/src/components/graph/KnowledgeGraph3D.tsx
  • frontend/src/components/screens/AdminAnalytics.test.tsx
  • frontend/src/test-utils/mockNextDynamic.tsx

📝 Walkthrough

Walkthrough

KnowledgeGraph now detects WebGL2 before selecting 3D mode, contains renderer failures locally, preserves the stored preference, and adds unit and Playwright regression coverage.

Changes

Knowledge graph WebGL fallback

Layer / File(s)Summary
WebGL2 capability and mode gating
frontend/src/components/graph/KnowledgeGraph.tsx, frontend/src/components/graph/KnowledgeGraph3D.tsx
The graph probes WebGL2, derives an effective mode without changing the stored preference, and disables the 3D toggle when WebGL2 is unavailable.
Local renderer failure recovery
frontend/src/components/graph/KnowledgeGraph.tsx, frontend/src/components/ErrorBoundary.tsx, docs/frontend-testids.md
A graph-local boundary shows retryable fallback UI, re-probes after 3D failures, and switches to 2D when capability is lost.
Regression coverage and test infrastructure
frontend/src/components/graph/KnowledgeGraph.test.tsx, frontend/e2e/graph-webgl-fallback.spec.ts, frontend/src/test-utils/mockNextDynamic.tsx, frontend/src/components/graph/*test.tsx, frontend/src/components/screens/AdminAnalytics.test.tsx
Tests cover mode selection, WebGL2 gating, synchronization, toggle accessibility, crash recovery, retries, and persisted preferences. Shared dynamic-import mocking replaces local implementations.

Estimated code review effort: 4 (Complex) | ~45 minutes

Mergeability Score:⚪ Minimal · up to d6cfa

The change gates 3D rendering on WebGL2 and falls back to the 2D graph when renderer creation fails; no actionable merge-blocking risk remains after normal checks and review.

Sequence Diagram(s)

sequenceDiagram
participant Browser
participant KnowledgeGraph
participant localStorage
participant KnowledgeGraph3D
participant KnowledgeGraph2D
Browser->>KnowledgeGraph: mount graph
KnowledgeGraph->>Browser: probe WebGL2
Browser-->>KnowledgeGraph: return capability result
KnowledgeGraph->>localStorage: read persisted mode
KnowledgeGraph->>KnowledgeGraph2D: render effective 2D mode when WebGL2 is unavailable
KnowledgeGraph-->>Browser: expose disabled 3D toggle explanation
KnowledgeGraph->>KnowledgeGraph3D: mount 3D renderer when supported
KnowledgeGraph3D-->>KnowledgeGraph: report renderer failure
KnowledgeGraph->>KnowledgeGraph2D: render local 2D fallback
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 25.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly and concisely identifies the primary WebGL2 gating and renderer crash-containment changes.
Description check✅ PassedThe description explains the bug, implementation, linked issue, tests, and verification, although it does not follow every template heading.
Linked Issues check✅ PassedThe changes satisfy [#538] by gating persisted 3D mode, preserving preferences, containing graph crashes, and adding regression tests.
Out of Scope Changes check✅ PassedAll changes support the WebGL2 fallback, graph crash containment, accessibility, documentation, or related test infrastructure objectives.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/538-graph-webgl-fallback

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/src/components/graph/KnowledgeGraph.tsx`:
- Line 141: In frontend/src/components/graph/KnowledgeGraph.tsx:141-141, wrap
the fallback KnowledgeGraph2D rendered by GraphCrashFallback for crashedMode ===
"3d" in a nested ErrorBoundary using the inline status placeholder as its
fallback, preventing 2D failures from reaching the root fallback. In
frontend/src/components/graph/KnowledgeGraph.test.tsx:30-50, add the throw2d
mock control; in frontend/src/components/graph/KnowledgeGraph.test.tsx:148-176,
add a regression test covering simultaneous 3D and 2D failures and asserting the
nested placeholder is rendered.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5c77b3cc-2ebe-41aa-af76-de2ad1793ce2

📥 Commits

Reviewing files that changed from the base of the PR and between 7681c48 and ce7c291.

📒 Files selected for processing (3)
  • frontend/e2e/graph-webgl-fallback.spec.ts
  • frontend/src/components/graph/KnowledgeGraph.test.tsx
  • frontend/src/components/graph/KnowledgeGraph.tsx

React.useEffect(() => {
if (crashedMode === "3d") heal("2d");
}, [crashedMode, heal]);
if (crashedMode === "3d") return <KnowledgeGraph2D {...graphProps} />;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -e
printf'%s\n''--- KnowledgeGraph.tsx outline ---'
ast-grep outline frontend/src/components/graph/KnowledgeGraph.tsx
printf'%s\n''--- Relevant implementation ---'
sed -n '1,230p' frontend/src/components/graph/KnowledgeGraph.tsx
printf'%s\n''--- Relevant test structure ---'
sed -n '1,230p' frontend/src/components/graph/KnowledgeGraph.test.tsx

Repository: SaplingLearn/Sapling

Length of output: 14232


🏁 Script executed:

#!/bin/bashset -e
printf'%s\n''--- ErrorBoundary definition and usages ---'
fd -i 'ErrorBoundary' frontend/src
rg -n -C 12 'class ErrorBoundary|function ErrorBoundary|<ErrorBoundary' frontend/src
printf'%s\n''--- GraphCrashFallback references ---'
rg -n -C 8 'GraphCrashFallback|crashedMode === "3d"|The graph couldn' frontend/src/components/graph

Repository: SaplingLearn/Sapling

Length of output: 10536


🏁 Script executed:

#!/bin/bashset -e
cat -n frontend/src/components/ErrorBoundary.tsx
printf'%s\n''--- package and test scripts ---'if [ -f frontend/package.json ];then
sed -n '1,220p' frontend/package.json
fiif [ -f package.json ];then
sed -n '1,180p' package.json
fi

Repository: SaplingLearn/Sapling

Length of output: 7291


🏁 Script executed:

#!/bin/bashset -e
python3 - <<'PY'from pathlib import Pathimpl = Path("frontend/src/components/graph/KnowledgeGraph.tsx").read_text()boundary = Path("frontend/src/components/ErrorBoundary.tsx").read_text()test = Path("frontend/src/components/graph/KnowledgeGraph.test.tsx").read_text()checks = { "outer boundary invokes GraphCrashFallback as its fallback": ( '<GraphCrashFallback crashedMode={mode} graphProps={props} heal={setMode} />' in impl ), "3D fallback directly renders 2D": ( 'if (crashedMode === "3d") return <KnowledgeGraph2D {...graphProps} />;' in impl ), "ErrorBoundary calls fallback from its own render": ( "if (this.props.fallback) return this.props.fallback(this.state.error, this.reset);" in boundary ), "2D mock has no throw2d control": ( "throw2d" not in test ), "existing crash test only arms 3D": ( "mockState.throw3d = true;" in test and "mockState.throw2d" not in test ),}for name, result in checks.items(): print(f"{'PASS' if result else 'FAIL'}: {name}")# This models the relevant React error-boundary rule:# an error thrown while an ErrorBoundary renders its own fallback# cannot be handled by that same boundary.print("MODEL: 3D child throws -> outer boundary renders GraphCrashFallback")print("MODEL: GraphCrashFallback renders 2D directly -> a 2D throw occurs in outer fallback render")print("MODEL: without a nested boundary, that throw propagates to the next ancestor boundary")PY

Repository: SaplingLearn/Sapling

Length of output: 637


Contain a 2D failure during the 3D fallback.

When KnowledgeGraph3D throws, GraphCrashFallback renders KnowledgeGraph2D from the outer ErrorBoundary fallback. A 2D error then propagates to the root fallback. Wrap the fallback 2D graph in a nested ErrorBoundary with the inline status placeholder as its fallback. Add a throw2d mock control and regression test for simultaneous 3D and 2D failures.

📍 Affects 2 files
  • frontend/src/components/graph/KnowledgeGraph.tsx#L141-L141 (this comment)
  • frontend/src/components/graph/KnowledgeGraph.test.tsx#L30-L50
  • frontend/src/components/graph/KnowledgeGraph.test.tsx#L148-L176
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/graph/KnowledgeGraph.tsx` at line 141, In
frontend/src/components/graph/KnowledgeGraph.tsx:141-141, wrap the fallback
KnowledgeGraph2D rendered by GraphCrashFallback for crashedMode === "3d" in a
nested ErrorBoundary using the inline status placeholder as its fallback,
preventing 2D failures from reaching the root fallback. In
frontend/src/components/graph/KnowledgeGraph.test.tsx:30-50, add the throw2d
mock control; in frontend/src/components/graph/KnowledgeGraph.test.tsx:148-176,
add a regression test covering simultaneous 3D and 2D failures and asserting the
nested placeholder is rendered.

…led toggle, hardened tests
Reworks the crash containment so the persisted mode is never rewritten:
the wrapper derives an EFFECTIVE mode (one gate encoding) from the raw
persisted wish + a module-memoized WebGL2 probe, and a 3D crash re-probes
capability instead of healing localStorage — capability really gone flips
the effective mode (boundary re-keys, 2D mounts fresh); transient crashes
get a static placeholder with the boundary's reset wired to Try again.
The fallback never renders another graph component (a throw during a
boundary's own fallback render is uncatchable by that boundary), and
crashes now log in production.
The toggle swaps native disabled for aria-disabled + aria-describedby so
keyboard/SR users can reach the "3D requires WebGL" reason; the action
stays the accessible name.
Test hardening from the review: crash sentinels throw from a layout
effect (the real #538 crash phase); getContext is stubbed via vi.spyOn so
restoreAllMocks actually restores; storage-key/sync-event constants are
exported and imported by the unit tests (journey mirrors by comment);
SYNC_EVENT gating has direct coverage; the journey anchors its negative
assertions on the new error-fallback testid, re-asserts end state, and
pins the positive path (toggle enabled under real WebGL); the four
divergent next/dynamic passthrough mocks collapse into a shared
hook-safe helper.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Applied all 15 code-review findings in d6cfaea (design rework where the review asked for one, not point patches):

  • Crash containment redesigned (findings 1–5): the persisted mode is now never rewritten. The wrapper derives one EFFECTIVE mode from the raw persisted wish + a module-memoized WebGL2 probe; a 3D crash re-probes capability (the only signal the cached verdict may be stale) — capability really gone flips the effective mode and re-keys the boundary so 2D mounts fresh; transient crashes show a static placeholder with the boundary's reset wired to "Try again". The fallback never renders another graph component (a throw in a boundary's own fallback render is uncatchable by that boundary), and renderer crashes now console.error in production.
  • A11y (11): aria-disabled + aria-describedby replace native disabled; the toggle stays focusable, keeps its action name, and the "3D requires WebGL" reason is reachable.
  • Simplification (9, 10): the gate rule has one render-time encoding (effective = webglOk ? mode : "2d"); the probe is memoized with an explicit force re-probe path.
  • Test hardening (6, 7, 8, 12, 13, 14, 15): crash sentinels throw from a layout effect (the real crash phase); vi.spyOn getContext stub actually restores; exported storage-key/sync-event constants (journey mirrors by comment); direct SYNC_EVENT gating coverage; journey negatives anchor on the new error-fallback testid with an end-state re-assert; a real-browser positive-path test pins the toggle enabled under genuine WebGL2; the four divergent next/dynamic passthrough mocks collapsed into one shared hook-safe helper.

Verification: unit suite 627/627, eslint 0 errors (stale suppressions pruned), tsc clean, full e2e lane 44/44 (both #538 journeys) + oracles clean, run from a fresh worktree via one flock'd up→test→down cycle.

🤖 Generated with Claude Code

@AndresL230
AndresL230 merged commit 5c11b0c into mainAug 13, 2026
6 of 7 checks passed
@AndresL230
AndresL230 deleted the fix/538-graph-webgl-fallback branch August 13, 2026 01:07
Sign up for freeto 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.

Dashboard crashes to the root error fallback on browsers without WebGL when 3D graph mode is persisted

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' fix(graph): gate 3D mode on WebGL2 and contain renderer crashes by AndresL230 · Pull Request #539 · SaplingLearn/Sapling · GitHub
Skip to content

fix(graph): gate 3D mode on WebGL2 and contain renderer crashes - #539

Merged
AndresL230 merged 2 commits into
mainfrom
fix/538-graph-webgl-fallback
Aug 13, 2026
Merged

fix(graph): gate 3D mode on WebGL2 and contain renderer crashes#539
AndresL230 merged 2 commits into
mainfrom
fix/538-graph-webgl-fallback

Conversation

@AndresL230

@AndresL230AndresL230 commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Fixes#538.

The bug

A browser without WebGL that lands on the dashboard (or Tree/Learn) with localStorage["sapling.kg.mode"] = "3d" persisted collapses the entire app into the root error fallback ("We hit a snag"), permanently — "Try again" re-throws. Chain:

  1. The KnowledgeGraph wrapper honoured the persisted "3d" unconditionally — no WebGL capability check existed anywhere in the frontend.
  2. react-force-graph-3d mounts its engine synchronously in a useLayoutEffect (react-kapsule), constructing new three.WebGLRenderer(...).
  3. three.js r163+ throwsError creating WebGL context. from the constructor when canvas.getContext("webgl2") returns null.
  4. The nearest boundary was the root one (app/layout.tsx), so the whole tree unmounted.

A profile that toggled 3D on a WebGL-capable browser was locked out of the app the moment WebGL went away (disabled, GPU blocklist, remote desktop, VM). Fresh profiles were unaffected (mode defaults to 2D) — which is why this read as "used to work".

The fix (both in the wrapper, so Dashboard/Tree/Learn are all covered)

  1. WebGL2 capability gate (webgl2Available()): probe canvas.getContext("webgl2") once per wrapper mount (probe context released via WEBGL_lose_context so probes never count toward the per-page context cap). Without WebGL2:
    • a persisted "3d" is ignored, not rewritten — the preference survives for the profile's WebGL-capable browsers;
    • the mode toggle renders disabled with title/aria-label "3D requires WebGL";
    • the storage/custom-event mode-sync paths and the setter are gated identically.
  2. Graph-local error boundary, keyed by mode: any residual renderer crash (context creation failing despite a successful probe — GPU process crash, context-limit exhaustion) degrades to the 2D graph inline and heals the persisted mode to "2d"; the heal flips the boundary key, clearing the error state into a normal 2D render. A (never-observed) 2D crash renders a minimal placeholder instead of remounting the thing that just threw.

Tests

  • Unit (KnowledgeGraph.test.tsx, 6 tests, written first and watched fail): fresh-profile default pin; 3d-honoured-with-WebGL pin; forces-2D-without-WebGL2; toggle disabled with explanatory label; toggle enabled with WebGL; crash containment + mode heal (RED run reproduced the exact production throw escaping uncaught).
  • Promoted journey (e2e/graph-webgl-fallback.spec.ts): init script nulls the webgl/webgl2 branches of getContext (exactly the API three probes; 2D canvas stays real) and seeds the poisoned "3d" mode → dashboard must render the 2D SVG graph, no root fallback, toggle disabled, preference preserved.

Verification

  • vitest run: 72 files / 624+ tests green (against a jsdom that is itself a no-WebGL environment, so every existing screen test now exercises the gated path).
  • tsc --noEmit clean; eslint clean on changed files (no new suppressions).
  • next build clean.
  • Pre-fix RED run (stack cycle 1): with the wrapper fix stashed and the real production build served, the new journey fails — the dashboard collapses into the root fallback exactly as reported. The journey failed at the "We hit a snag" count-0 assertion (exit 1) — the root fallback was live on the page.
  • Post-fix full lane (stack cycle 2): entire Chapter-1 Playwright lane + deterministic oracles green. 43 passed (1.5m), oracles exit 0.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved graph rendering when WebGL2 is unavailable or 3D rendering fails.
    • Automatically falls back to the 2D graph while preserving the saved mode preference.
    • Disabled the 3D toggle when unsupported, with an explanatory message.
    • Added recovery and retry handling for temporary graph rendering failures.
    • Prevented graph errors from disrupting the rest of the dashboard.
  • Tests

    • Added coverage for WebGL detection, fallback behavior, mode synchronization, toggle states, and renderer recovery.
    • Added end-to-end coverage for 2D fallback and supported 3D environments.

A browser without WebGL landing on any graph surface with a persisted
"3d" mode collapsed the whole app into the root error fallback: the
wrapper honoured localStorage unconditionally, three r163+ throws from
the WebGLRenderer constructor in a mount-time layout effect, and the
only boundary above it was the root one.
The wrapper now probes canvas.getContext("webgl2") before honouring a
persisted "3d" (ignoring, not rewriting, the preference), disables the
mode toggle with "3D requires WebGL" when the probe fails, and wraps
the graph slot in a mode-keyed local ErrorBoundary whose fallback
degrades a crashed 3D renderer to the 2D graph and heals the persisted
mode to "2d".
Unit tests were written first and watched fail (the crash test
reproduced the exact production throw escaping uncaught); the promoted
journey simulates a no-WebGL browser via an init-script getContext
override and failed against a pre-fix stack build with the dashboard on
the root fallback, exactly as reported.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 12, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-stagingd6cfaeaCommit Preview URL

Branch Preview URL
Aug 13 2026, 01:07 AM

@supabase

supabaseBot commented Aug 12, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: feb9f6c1-33ca-40db-986b-72181daea902

📥 Commits

Reviewing files that changed from the base of the PR and between ce7c291 and d6cfaea.

📒 Files selected for processing (11)
  • docs/frontend-testids.md
  • frontend/e2e/graph-webgl-fallback.spec.ts
  • frontend/eslint-suppressions.json
  • frontend/src/components/ErrorBoundary.tsx
  • frontend/src/components/graph/KnowledgeGraph.test.tsx
  • frontend/src/components/graph/KnowledgeGraph.tsx
  • frontend/src/components/graph/KnowledgeGraph3D.test.tsx
  • frontend/src/components/graph/KnowledgeGraph3D.testmode.test.tsx
  • frontend/src/components/graph/KnowledgeGraph3D.tsx
  • frontend/src/components/screens/AdminAnalytics.test.tsx
  • frontend/src/test-utils/mockNextDynamic.tsx

📝 Walkthrough

Walkthrough

KnowledgeGraph now detects WebGL2 before selecting 3D mode, contains renderer failures locally, preserves the stored preference, and adds unit and Playwright regression coverage.

Changes

Knowledge graph WebGL fallback

Layer / File(s)Summary
WebGL2 capability and mode gating
frontend/src/components/graph/KnowledgeGraph.tsx, frontend/src/components/graph/KnowledgeGraph3D.tsx
The graph probes WebGL2, derives an effective mode without changing the stored preference, and disables the 3D toggle when WebGL2 is unavailable.
Local renderer failure recovery
frontend/src/components/graph/KnowledgeGraph.tsx, frontend/src/components/ErrorBoundary.tsx, docs/frontend-testids.md
A graph-local boundary shows retryable fallback UI, re-probes after 3D failures, and switches to 2D when capability is lost.
Regression coverage and test infrastructure
frontend/src/components/graph/KnowledgeGraph.test.tsx, frontend/e2e/graph-webgl-fallback.spec.ts, frontend/src/test-utils/mockNextDynamic.tsx, frontend/src/components/graph/*test.tsx, frontend/src/components/screens/AdminAnalytics.test.tsx
Tests cover mode selection, WebGL2 gating, synchronization, toggle accessibility, crash recovery, retries, and persisted preferences. Shared dynamic-import mocking replaces local implementations.

Estimated code review effort: 4 (Complex) | ~45 minutes

Mergeability Score:⚪ Minimal · up to d6cfa

The change gates 3D rendering on WebGL2 and falls back to the 2D graph when renderer creation fails; no actionable merge-blocking risk remains after normal checks and review.

Sequence Diagram(s)

sequenceDiagram
participant Browser
participant KnowledgeGraph
participant localStorage
participant KnowledgeGraph3D
participant KnowledgeGraph2D
Browser->>KnowledgeGraph: mount graph
KnowledgeGraph->>Browser: probe WebGL2
Browser-->>KnowledgeGraph: return capability result
KnowledgeGraph->>localStorage: read persisted mode
KnowledgeGraph->>KnowledgeGraph2D: render effective 2D mode when WebGL2 is unavailable
KnowledgeGraph-->>Browser: expose disabled 3D toggle explanation
KnowledgeGraph->>KnowledgeGraph3D: mount 3D renderer when supported
KnowledgeGraph3D-->>KnowledgeGraph: report renderer failure
KnowledgeGraph->>KnowledgeGraph2D: render local 2D fallback
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 25.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly and concisely identifies the primary WebGL2 gating and renderer crash-containment changes.
Description check✅ PassedThe description explains the bug, implementation, linked issue, tests, and verification, although it does not follow every template heading.
Linked Issues check✅ PassedThe changes satisfy [#538] by gating persisted 3D mode, preserving preferences, containing graph crashes, and adding regression tests.
Out of Scope Changes check✅ PassedAll changes support the WebGL2 fallback, graph crash containment, accessibility, documentation, or related test infrastructure objectives.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/538-graph-webgl-fallback

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/src/components/graph/KnowledgeGraph.tsx`:
- Line 141: In frontend/src/components/graph/KnowledgeGraph.tsx:141-141, wrap
the fallback KnowledgeGraph2D rendered by GraphCrashFallback for crashedMode ===
"3d" in a nested ErrorBoundary using the inline status placeholder as its
fallback, preventing 2D failures from reaching the root fallback. In
frontend/src/components/graph/KnowledgeGraph.test.tsx:30-50, add the throw2d
mock control; in frontend/src/components/graph/KnowledgeGraph.test.tsx:148-176,
add a regression test covering simultaneous 3D and 2D failures and asserting the
nested placeholder is rendered.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5c77b3cc-2ebe-41aa-af76-de2ad1793ce2

📥 Commits

Reviewing files that changed from the base of the PR and between 7681c48 and ce7c291.

📒 Files selected for processing (3)
  • frontend/e2e/graph-webgl-fallback.spec.ts
  • frontend/src/components/graph/KnowledgeGraph.test.tsx
  • frontend/src/components/graph/KnowledgeGraph.tsx

React.useEffect(() => {
if (crashedMode === "3d") heal("2d");
}, [crashedMode, heal]);
if (crashedMode === "3d") return <KnowledgeGraph2D {...graphProps} />;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -e
printf'%s\n''--- KnowledgeGraph.tsx outline ---'
ast-grep outline frontend/src/components/graph/KnowledgeGraph.tsx
printf'%s\n''--- Relevant implementation ---'
sed -n '1,230p' frontend/src/components/graph/KnowledgeGraph.tsx
printf'%s\n''--- Relevant test structure ---'
sed -n '1,230p' frontend/src/components/graph/KnowledgeGraph.test.tsx

Repository: SaplingLearn/Sapling

Length of output: 14232


🏁 Script executed:

#!/bin/bashset -e
printf'%s\n''--- ErrorBoundary definition and usages ---'
fd -i 'ErrorBoundary' frontend/src
rg -n -C 12 'class ErrorBoundary|function ErrorBoundary|<ErrorBoundary' frontend/src
printf'%s\n''--- GraphCrashFallback references ---'
rg -n -C 8 'GraphCrashFallback|crashedMode === "3d"|The graph couldn' frontend/src/components/graph

Repository: SaplingLearn/Sapling

Length of output: 10536


🏁 Script executed:

#!/bin/bashset -e
cat -n frontend/src/components/ErrorBoundary.tsx
printf'%s\n''--- package and test scripts ---'if [ -f frontend/package.json ];then
sed -n '1,220p' frontend/package.json
fiif [ -f package.json ];then
sed -n '1,180p' package.json
fi

Repository: SaplingLearn/Sapling

Length of output: 7291


🏁 Script executed:

#!/bin/bashset -e
python3 - <<'PY'from pathlib import Pathimpl = Path("frontend/src/components/graph/KnowledgeGraph.tsx").read_text()boundary = Path("frontend/src/components/ErrorBoundary.tsx").read_text()test = Path("frontend/src/components/graph/KnowledgeGraph.test.tsx").read_text()checks = { "outer boundary invokes GraphCrashFallback as its fallback": ( '<GraphCrashFallback crashedMode={mode} graphProps={props} heal={setMode} />' in impl ), "3D fallback directly renders 2D": ( 'if (crashedMode === "3d") return <KnowledgeGraph2D {...graphProps} />;' in impl ), "ErrorBoundary calls fallback from its own render": ( "if (this.props.fallback) return this.props.fallback(this.state.error, this.reset);" in boundary ), "2D mock has no throw2d control": ( "throw2d" not in test ), "existing crash test only arms 3D": ( "mockState.throw3d = true;" in test and "mockState.throw2d" not in test ),}for name, result in checks.items(): print(f"{'PASS' if result else 'FAIL'}: {name}")# This models the relevant React error-boundary rule:# an error thrown while an ErrorBoundary renders its own fallback# cannot be handled by that same boundary.print("MODEL: 3D child throws -> outer boundary renders GraphCrashFallback")print("MODEL: GraphCrashFallback renders 2D directly -> a 2D throw occurs in outer fallback render")print("MODEL: without a nested boundary, that throw propagates to the next ancestor boundary")PY

Repository: SaplingLearn/Sapling

Length of output: 637


Contain a 2D failure during the 3D fallback.

When KnowledgeGraph3D throws, GraphCrashFallback renders KnowledgeGraph2D from the outer ErrorBoundary fallback. A 2D error then propagates to the root fallback. Wrap the fallback 2D graph in a nested ErrorBoundary with the inline status placeholder as its fallback. Add a throw2d mock control and regression test for simultaneous 3D and 2D failures.

📍 Affects 2 files
  • frontend/src/components/graph/KnowledgeGraph.tsx#L141-L141 (this comment)
  • frontend/src/components/graph/KnowledgeGraph.test.tsx#L30-L50
  • frontend/src/components/graph/KnowledgeGraph.test.tsx#L148-L176
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/graph/KnowledgeGraph.tsx` at line 141, In
frontend/src/components/graph/KnowledgeGraph.tsx:141-141, wrap the fallback
KnowledgeGraph2D rendered by GraphCrashFallback for crashedMode === "3d" in a
nested ErrorBoundary using the inline status placeholder as its fallback,
preventing 2D failures from reaching the root fallback. In
frontend/src/components/graph/KnowledgeGraph.test.tsx:30-50, add the throw2d
mock control; in frontend/src/components/graph/KnowledgeGraph.test.tsx:148-176,
add a regression test covering simultaneous 3D and 2D failures and asserting the
nested placeholder is rendered.

…led toggle, hardened tests
Reworks the crash containment so the persisted mode is never rewritten:
the wrapper derives an EFFECTIVE mode (one gate encoding) from the raw
persisted wish + a module-memoized WebGL2 probe, and a 3D crash re-probes
capability instead of healing localStorage — capability really gone flips
the effective mode (boundary re-keys, 2D mounts fresh); transient crashes
get a static placeholder with the boundary's reset wired to Try again.
The fallback never renders another graph component (a throw during a
boundary's own fallback render is uncatchable by that boundary), and
crashes now log in production.
The toggle swaps native disabled for aria-disabled + aria-describedby so
keyboard/SR users can reach the "3D requires WebGL" reason; the action
stays the accessible name.
Test hardening from the review: crash sentinels throw from a layout
effect (the real #538 crash phase); getContext is stubbed via vi.spyOn so
restoreAllMocks actually restores; storage-key/sync-event constants are
exported and imported by the unit tests (journey mirrors by comment);
SYNC_EVENT gating has direct coverage; the journey anchors its negative
assertions on the new error-fallback testid, re-asserts end state, and
pins the positive path (toggle enabled under real WebGL); the four
divergent next/dynamic passthrough mocks collapse into a shared
hook-safe helper.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Applied all 15 code-review findings in d6cfaea (design rework where the review asked for one, not point patches):

  • Crash containment redesigned (findings 1–5): the persisted mode is now never rewritten. The wrapper derives one EFFECTIVE mode from the raw persisted wish + a module-memoized WebGL2 probe; a 3D crash re-probes capability (the only signal the cached verdict may be stale) — capability really gone flips the effective mode and re-keys the boundary so 2D mounts fresh; transient crashes show a static placeholder with the boundary's reset wired to "Try again". The fallback never renders another graph component (a throw in a boundary's own fallback render is uncatchable by that boundary), and renderer crashes now console.error in production.
  • A11y (11): aria-disabled + aria-describedby replace native disabled; the toggle stays focusable, keeps its action name, and the "3D requires WebGL" reason is reachable.
  • Simplification (9, 10): the gate rule has one render-time encoding (effective = webglOk ? mode : "2d"); the probe is memoized with an explicit force re-probe path.
  • Test hardening (6, 7, 8, 12, 13, 14, 15): crash sentinels throw from a layout effect (the real crash phase); vi.spyOn getContext stub actually restores; exported storage-key/sync-event constants (journey mirrors by comment); direct SYNC_EVENT gating coverage; journey negatives anchor on the new error-fallback testid with an end-state re-assert; a real-browser positive-path test pins the toggle enabled under genuine WebGL2; the four divergent next/dynamic passthrough mocks collapsed into one shared hook-safe helper.

Verification: unit suite 627/627, eslint 0 errors (stale suppressions pruned), tsc clean, full e2e lane 44/44 (both #538 journeys) + oracles clean, run from a fresh worktree via one flock'd up→test→down cycle.

🤖 Generated with Claude Code

@AndresL230
AndresL230 merged commit 5c11b0c into mainAug 13, 2026
6 of 7 checks passed
@AndresL230
AndresL230 deleted the fix/538-graph-webgl-fallback branch August 13, 2026 01:07
Sign up for freeto 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.

Dashboard crashes to the root error fallback on browsers without WebGL when 3D graph mode is persisted

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(graph): gate 3D mode on WebGL2 and contain renderer crashes by AndresL230 · Pull Request #539 · SaplingLearn/Sapling · GitHub
Skip to content

fix(graph): gate 3D mode on WebGL2 and contain renderer crashes - #539

Merged
AndresL230 merged 2 commits into
mainfrom
fix/538-graph-webgl-fallback
Aug 13, 2026
Merged

fix(graph): gate 3D mode on WebGL2 and contain renderer crashes#539
AndresL230 merged 2 commits into
mainfrom
fix/538-graph-webgl-fallback

Conversation

@AndresL230

@AndresL230AndresL230 commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Fixes#538.

The bug

A browser without WebGL that lands on the dashboard (or Tree/Learn) with localStorage["sapling.kg.mode"] = "3d" persisted collapses the entire app into the root error fallback ("We hit a snag"), permanently — "Try again" re-throws. Chain:

  1. The KnowledgeGraph wrapper honoured the persisted "3d" unconditionally — no WebGL capability check existed anywhere in the frontend.
  2. react-force-graph-3d mounts its engine synchronously in a useLayoutEffect (react-kapsule), constructing new three.WebGLRenderer(...).
  3. three.js r163+ throwsError creating WebGL context. from the constructor when canvas.getContext("webgl2") returns null.
  4. The nearest boundary was the root one (app/layout.tsx), so the whole tree unmounted.

A profile that toggled 3D on a WebGL-capable browser was locked out of the app the moment WebGL went away (disabled, GPU blocklist, remote desktop, VM). Fresh profiles were unaffected (mode defaults to 2D) — which is why this read as "used to work".

The fix (both in the wrapper, so Dashboard/Tree/Learn are all covered)

  1. WebGL2 capability gate (webgl2Available()): probe canvas.getContext("webgl2") once per wrapper mount (probe context released via WEBGL_lose_context so probes never count toward the per-page context cap). Without WebGL2:
    • a persisted "3d" is ignored, not rewritten — the preference survives for the profile's WebGL-capable browsers;
    • the mode toggle renders disabled with title/aria-label "3D requires WebGL";
    • the storage/custom-event mode-sync paths and the setter are gated identically.
  2. Graph-local error boundary, keyed by mode: any residual renderer crash (context creation failing despite a successful probe — GPU process crash, context-limit exhaustion) degrades to the 2D graph inline and heals the persisted mode to "2d"; the heal flips the boundary key, clearing the error state into a normal 2D render. A (never-observed) 2D crash renders a minimal placeholder instead of remounting the thing that just threw.

Tests

  • Unit (KnowledgeGraph.test.tsx, 6 tests, written first and watched fail): fresh-profile default pin; 3d-honoured-with-WebGL pin; forces-2D-without-WebGL2; toggle disabled with explanatory label; toggle enabled with WebGL; crash containment + mode heal (RED run reproduced the exact production throw escaping uncaught).
  • Promoted journey (e2e/graph-webgl-fallback.spec.ts): init script nulls the webgl/webgl2 branches of getContext (exactly the API three probes; 2D canvas stays real) and seeds the poisoned "3d" mode → dashboard must render the 2D SVG graph, no root fallback, toggle disabled, preference preserved.

Verification

  • vitest run: 72 files / 624+ tests green (against a jsdom that is itself a no-WebGL environment, so every existing screen test now exercises the gated path).
  • tsc --noEmit clean; eslint clean on changed files (no new suppressions).
  • next build clean.
  • Pre-fix RED run (stack cycle 1): with the wrapper fix stashed and the real production build served, the new journey fails — the dashboard collapses into the root fallback exactly as reported. The journey failed at the "We hit a snag" count-0 assertion (exit 1) — the root fallback was live on the page.
  • Post-fix full lane (stack cycle 2): entire Chapter-1 Playwright lane + deterministic oracles green. 43 passed (1.5m), oracles exit 0.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved graph rendering when WebGL2 is unavailable or 3D rendering fails.
    • Automatically falls back to the 2D graph while preserving the saved mode preference.
    • Disabled the 3D toggle when unsupported, with an explanatory message.
    • Added recovery and retry handling for temporary graph rendering failures.
    • Prevented graph errors from disrupting the rest of the dashboard.
  • Tests

    • Added coverage for WebGL detection, fallback behavior, mode synchronization, toggle states, and renderer recovery.
    • Added end-to-end coverage for 2D fallback and supported 3D environments.

A browser without WebGL landing on any graph surface with a persisted
"3d" mode collapsed the whole app into the root error fallback: the
wrapper honoured localStorage unconditionally, three r163+ throws from
the WebGLRenderer constructor in a mount-time layout effect, and the
only boundary above it was the root one.
The wrapper now probes canvas.getContext("webgl2") before honouring a
persisted "3d" (ignoring, not rewriting, the preference), disables the
mode toggle with "3D requires WebGL" when the probe fails, and wraps
the graph slot in a mode-keyed local ErrorBoundary whose fallback
degrades a crashed 3D renderer to the 2D graph and heals the persisted
mode to "2d".
Unit tests were written first and watched fail (the crash test
reproduced the exact production throw escaping uncaught); the promoted
journey simulates a no-WebGL browser via an init-script getContext
override and failed against a pre-fix stack build with the dashboard on
the root fallback, exactly as reported.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 12, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-stagingd6cfaeaCommit Preview URL

Branch Preview URL
Aug 13 2026, 01:07 AM

@supabase

supabaseBot commented Aug 12, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: feb9f6c1-33ca-40db-986b-72181daea902

📥 Commits

Reviewing files that changed from the base of the PR and between ce7c291 and d6cfaea.

📒 Files selected for processing (11)
  • docs/frontend-testids.md
  • frontend/e2e/graph-webgl-fallback.spec.ts
  • frontend/eslint-suppressions.json
  • frontend/src/components/ErrorBoundary.tsx
  • frontend/src/components/graph/KnowledgeGraph.test.tsx
  • frontend/src/components/graph/KnowledgeGraph.tsx
  • frontend/src/components/graph/KnowledgeGraph3D.test.tsx
  • frontend/src/components/graph/KnowledgeGraph3D.testmode.test.tsx
  • frontend/src/components/graph/KnowledgeGraph3D.tsx
  • frontend/src/components/screens/AdminAnalytics.test.tsx
  • frontend/src/test-utils/mockNextDynamic.tsx

📝 Walkthrough

Walkthrough

KnowledgeGraph now detects WebGL2 before selecting 3D mode, contains renderer failures locally, preserves the stored preference, and adds unit and Playwright regression coverage.

Changes

Knowledge graph WebGL fallback

Layer / File(s)Summary
WebGL2 capability and mode gating
frontend/src/components/graph/KnowledgeGraph.tsx, frontend/src/components/graph/KnowledgeGraph3D.tsx
The graph probes WebGL2, derives an effective mode without changing the stored preference, and disables the 3D toggle when WebGL2 is unavailable.
Local renderer failure recovery
frontend/src/components/graph/KnowledgeGraph.tsx, frontend/src/components/ErrorBoundary.tsx, docs/frontend-testids.md
A graph-local boundary shows retryable fallback UI, re-probes after 3D failures, and switches to 2D when capability is lost.
Regression coverage and test infrastructure
frontend/src/components/graph/KnowledgeGraph.test.tsx, frontend/e2e/graph-webgl-fallback.spec.ts, frontend/src/test-utils/mockNextDynamic.tsx, frontend/src/components/graph/*test.tsx, frontend/src/components/screens/AdminAnalytics.test.tsx
Tests cover mode selection, WebGL2 gating, synchronization, toggle accessibility, crash recovery, retries, and persisted preferences. Shared dynamic-import mocking replaces local implementations.

Estimated code review effort: 4 (Complex) | ~45 minutes

Mergeability Score:⚪ Minimal · up to d6cfa

The change gates 3D rendering on WebGL2 and falls back to the 2D graph when renderer creation fails; no actionable merge-blocking risk remains after normal checks and review.

Sequence Diagram(s)

sequenceDiagram
participant Browser
participant KnowledgeGraph
participant localStorage
participant KnowledgeGraph3D
participant KnowledgeGraph2D
Browser->>KnowledgeGraph: mount graph
KnowledgeGraph->>Browser: probe WebGL2
Browser-->>KnowledgeGraph: return capability result
KnowledgeGraph->>localStorage: read persisted mode
KnowledgeGraph->>KnowledgeGraph2D: render effective 2D mode when WebGL2 is unavailable
KnowledgeGraph-->>Browser: expose disabled 3D toggle explanation
KnowledgeGraph->>KnowledgeGraph3D: mount 3D renderer when supported
KnowledgeGraph3D-->>KnowledgeGraph: report renderer failure
KnowledgeGraph->>KnowledgeGraph2D: render local 2D fallback
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 25.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly and concisely identifies the primary WebGL2 gating and renderer crash-containment changes.
Description check✅ PassedThe description explains the bug, implementation, linked issue, tests, and verification, although it does not follow every template heading.
Linked Issues check✅ PassedThe changes satisfy [#538] by gating persisted 3D mode, preserving preferences, containing graph crashes, and adding regression tests.
Out of Scope Changes check✅ PassedAll changes support the WebGL2 fallback, graph crash containment, accessibility, documentation, or related test infrastructure objectives.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/538-graph-webgl-fallback

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/src/components/graph/KnowledgeGraph.tsx`:
- Line 141: In frontend/src/components/graph/KnowledgeGraph.tsx:141-141, wrap
the fallback KnowledgeGraph2D rendered by GraphCrashFallback for crashedMode ===
"3d" in a nested ErrorBoundary using the inline status placeholder as its
fallback, preventing 2D failures from reaching the root fallback. In
frontend/src/components/graph/KnowledgeGraph.test.tsx:30-50, add the throw2d
mock control; in frontend/src/components/graph/KnowledgeGraph.test.tsx:148-176,
add a regression test covering simultaneous 3D and 2D failures and asserting the
nested placeholder is rendered.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5c77b3cc-2ebe-41aa-af76-de2ad1793ce2

📥 Commits

Reviewing files that changed from the base of the PR and between 7681c48 and ce7c291.

📒 Files selected for processing (3)
  • frontend/e2e/graph-webgl-fallback.spec.ts
  • frontend/src/components/graph/KnowledgeGraph.test.tsx
  • frontend/src/components/graph/KnowledgeGraph.tsx

React.useEffect(() => {
if (crashedMode === "3d") heal("2d");
}, [crashedMode, heal]);
if (crashedMode === "3d") return <KnowledgeGraph2D {...graphProps} />;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -e
printf'%s\n''--- KnowledgeGraph.tsx outline ---'
ast-grep outline frontend/src/components/graph/KnowledgeGraph.tsx
printf'%s\n''--- Relevant implementation ---'
sed -n '1,230p' frontend/src/components/graph/KnowledgeGraph.tsx
printf'%s\n''--- Relevant test structure ---'
sed -n '1,230p' frontend/src/components/graph/KnowledgeGraph.test.tsx

Repository: SaplingLearn/Sapling

Length of output: 14232


🏁 Script executed:

#!/bin/bashset -e
printf'%s\n''--- ErrorBoundary definition and usages ---'
fd -i 'ErrorBoundary' frontend/src
rg -n -C 12 'class ErrorBoundary|function ErrorBoundary|<ErrorBoundary' frontend/src
printf'%s\n''--- GraphCrashFallback references ---'
rg -n -C 8 'GraphCrashFallback|crashedMode === "3d"|The graph couldn' frontend/src/components/graph

Repository: SaplingLearn/Sapling

Length of output: 10536


🏁 Script executed:

#!/bin/bashset -e
cat -n frontend/src/components/ErrorBoundary.tsx
printf'%s\n''--- package and test scripts ---'if [ -f frontend/package.json ];then
sed -n '1,220p' frontend/package.json
fiif [ -f package.json ];then
sed -n '1,180p' package.json
fi

Repository: SaplingLearn/Sapling

Length of output: 7291


🏁 Script executed:

#!/bin/bashset -e
python3 - <<'PY'from pathlib import Pathimpl = Path("frontend/src/components/graph/KnowledgeGraph.tsx").read_text()boundary = Path("frontend/src/components/ErrorBoundary.tsx").read_text()test = Path("frontend/src/components/graph/KnowledgeGraph.test.tsx").read_text()checks = { "outer boundary invokes GraphCrashFallback as its fallback": ( '<GraphCrashFallback crashedMode={mode} graphProps={props} heal={setMode} />' in impl ), "3D fallback directly renders 2D": ( 'if (crashedMode === "3d") return <KnowledgeGraph2D {...graphProps} />;' in impl ), "ErrorBoundary calls fallback from its own render": ( "if (this.props.fallback) return this.props.fallback(this.state.error, this.reset);" in boundary ), "2D mock has no throw2d control": ( "throw2d" not in test ), "existing crash test only arms 3D": ( "mockState.throw3d = true;" in test and "mockState.throw2d" not in test ),}for name, result in checks.items(): print(f"{'PASS' if result else 'FAIL'}: {name}")# This models the relevant React error-boundary rule:# an error thrown while an ErrorBoundary renders its own fallback# cannot be handled by that same boundary.print("MODEL: 3D child throws -> outer boundary renders GraphCrashFallback")print("MODEL: GraphCrashFallback renders 2D directly -> a 2D throw occurs in outer fallback render")print("MODEL: without a nested boundary, that throw propagates to the next ancestor boundary")PY

Repository: SaplingLearn/Sapling

Length of output: 637


Contain a 2D failure during the 3D fallback.

When KnowledgeGraph3D throws, GraphCrashFallback renders KnowledgeGraph2D from the outer ErrorBoundary fallback. A 2D error then propagates to the root fallback. Wrap the fallback 2D graph in a nested ErrorBoundary with the inline status placeholder as its fallback. Add a throw2d mock control and regression test for simultaneous 3D and 2D failures.

📍 Affects 2 files
  • frontend/src/components/graph/KnowledgeGraph.tsx#L141-L141 (this comment)
  • frontend/src/components/graph/KnowledgeGraph.test.tsx#L30-L50
  • frontend/src/components/graph/KnowledgeGraph.test.tsx#L148-L176
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/graph/KnowledgeGraph.tsx` at line 141, In
frontend/src/components/graph/KnowledgeGraph.tsx:141-141, wrap the fallback
KnowledgeGraph2D rendered by GraphCrashFallback for crashedMode === "3d" in a
nested ErrorBoundary using the inline status placeholder as its fallback,
preventing 2D failures from reaching the root fallback. In
frontend/src/components/graph/KnowledgeGraph.test.tsx:30-50, add the throw2d
mock control; in frontend/src/components/graph/KnowledgeGraph.test.tsx:148-176,
add a regression test covering simultaneous 3D and 2D failures and asserting the
nested placeholder is rendered.

…led toggle, hardened tests
Reworks the crash containment so the persisted mode is never rewritten:
the wrapper derives an EFFECTIVE mode (one gate encoding) from the raw
persisted wish + a module-memoized WebGL2 probe, and a 3D crash re-probes
capability instead of healing localStorage — capability really gone flips
the effective mode (boundary re-keys, 2D mounts fresh); transient crashes
get a static placeholder with the boundary's reset wired to Try again.
The fallback never renders another graph component (a throw during a
boundary's own fallback render is uncatchable by that boundary), and
crashes now log in production.
The toggle swaps native disabled for aria-disabled + aria-describedby so
keyboard/SR users can reach the "3D requires WebGL" reason; the action
stays the accessible name.
Test hardening from the review: crash sentinels throw from a layout
effect (the real #538 crash phase); getContext is stubbed via vi.spyOn so
restoreAllMocks actually restores; storage-key/sync-event constants are
exported and imported by the unit tests (journey mirrors by comment);
SYNC_EVENT gating has direct coverage; the journey anchors its negative
assertions on the new error-fallback testid, re-asserts end state, and
pins the positive path (toggle enabled under real WebGL); the four
divergent next/dynamic passthrough mocks collapse into a shared
hook-safe helper.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Applied all 15 code-review findings in d6cfaea (design rework where the review asked for one, not point patches):

  • Crash containment redesigned (findings 1–5): the persisted mode is now never rewritten. The wrapper derives one EFFECTIVE mode from the raw persisted wish + a module-memoized WebGL2 probe; a 3D crash re-probes capability (the only signal the cached verdict may be stale) — capability really gone flips the effective mode and re-keys the boundary so 2D mounts fresh; transient crashes show a static placeholder with the boundary's reset wired to "Try again". The fallback never renders another graph component (a throw in a boundary's own fallback render is uncatchable by that boundary), and renderer crashes now console.error in production.
  • A11y (11): aria-disabled + aria-describedby replace native disabled; the toggle stays focusable, keeps its action name, and the "3D requires WebGL" reason is reachable.
  • Simplification (9, 10): the gate rule has one render-time encoding (effective = webglOk ? mode : "2d"); the probe is memoized with an explicit force re-probe path.
  • Test hardening (6, 7, 8, 12, 13, 14, 15): crash sentinels throw from a layout effect (the real crash phase); vi.spyOn getContext stub actually restores; exported storage-key/sync-event constants (journey mirrors by comment); direct SYNC_EVENT gating coverage; journey negatives anchor on the new error-fallback testid with an end-state re-assert; a real-browser positive-path test pins the toggle enabled under genuine WebGL2; the four divergent next/dynamic passthrough mocks collapsed into one shared hook-safe helper.

Verification: unit suite 627/627, eslint 0 errors (stale suppressions pruned), tsc clean, full e2e lane 44/44 (both #538 journeys) + oracles clean, run from a fresh worktree via one flock'd up→test→down cycle.

🤖 Generated with Claude Code

@AndresL230
AndresL230 merged commit 5c11b0c into mainAug 13, 2026
6 of 7 checks passed
@AndresL230
AndresL230 deleted the fix/538-graph-webgl-fallback branch August 13, 2026 01:07
Sign up for freeto 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.

Dashboard crashes to the root error fallback on browsers without WebGL when 3D graph mode is persisted

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(graph): gate 3D mode on WebGL2 and contain renderer crashes by AndresL230 · Pull Request #539 · SaplingLearn/Sapling · GitHub
Skip to content

fix(graph): gate 3D mode on WebGL2 and contain renderer crashes - #539

Merged
AndresL230 merged 2 commits into
mainfrom
fix/538-graph-webgl-fallback
Aug 13, 2026
Merged

fix(graph): gate 3D mode on WebGL2 and contain renderer crashes#539
AndresL230 merged 2 commits into
mainfrom
fix/538-graph-webgl-fallback

Conversation

@AndresL230

@AndresL230AndresL230 commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Fixes#538.

The bug

A browser without WebGL that lands on the dashboard (or Tree/Learn) with localStorage["sapling.kg.mode"] = "3d" persisted collapses the entire app into the root error fallback ("We hit a snag"), permanently — "Try again" re-throws. Chain:

  1. The KnowledgeGraph wrapper honoured the persisted "3d" unconditionally — no WebGL capability check existed anywhere in the frontend.
  2. react-force-graph-3d mounts its engine synchronously in a useLayoutEffect (react-kapsule), constructing new three.WebGLRenderer(...).
  3. three.js r163+ throwsError creating WebGL context. from the constructor when canvas.getContext("webgl2") returns null.
  4. The nearest boundary was the root one (app/layout.tsx), so the whole tree unmounted.

A profile that toggled 3D on a WebGL-capable browser was locked out of the app the moment WebGL went away (disabled, GPU blocklist, remote desktop, VM). Fresh profiles were unaffected (mode defaults to 2D) — which is why this read as "used to work".

The fix (both in the wrapper, so Dashboard/Tree/Learn are all covered)

  1. WebGL2 capability gate (webgl2Available()): probe canvas.getContext("webgl2") once per wrapper mount (probe context released via WEBGL_lose_context so probes never count toward the per-page context cap). Without WebGL2:
    • a persisted "3d" is ignored, not rewritten — the preference survives for the profile's WebGL-capable browsers;
    • the mode toggle renders disabled with title/aria-label "3D requires WebGL";
    • the storage/custom-event mode-sync paths and the setter are gated identically.
  2. Graph-local error boundary, keyed by mode: any residual renderer crash (context creation failing despite a successful probe — GPU process crash, context-limit exhaustion) degrades to the 2D graph inline and heals the persisted mode to "2d"; the heal flips the boundary key, clearing the error state into a normal 2D render. A (never-observed) 2D crash renders a minimal placeholder instead of remounting the thing that just threw.

Tests

  • Unit (KnowledgeGraph.test.tsx, 6 tests, written first and watched fail): fresh-profile default pin; 3d-honoured-with-WebGL pin; forces-2D-without-WebGL2; toggle disabled with explanatory label; toggle enabled with WebGL; crash containment + mode heal (RED run reproduced the exact production throw escaping uncaught).
  • Promoted journey (e2e/graph-webgl-fallback.spec.ts): init script nulls the webgl/webgl2 branches of getContext (exactly the API three probes; 2D canvas stays real) and seeds the poisoned "3d" mode → dashboard must render the 2D SVG graph, no root fallback, toggle disabled, preference preserved.

Verification

  • vitest run: 72 files / 624+ tests green (against a jsdom that is itself a no-WebGL environment, so every existing screen test now exercises the gated path).
  • tsc --noEmit clean; eslint clean on changed files (no new suppressions).
  • next build clean.
  • Pre-fix RED run (stack cycle 1): with the wrapper fix stashed and the real production build served, the new journey fails — the dashboard collapses into the root fallback exactly as reported. The journey failed at the "We hit a snag" count-0 assertion (exit 1) — the root fallback was live on the page.
  • Post-fix full lane (stack cycle 2): entire Chapter-1 Playwright lane + deterministic oracles green. 43 passed (1.5m), oracles exit 0.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved graph rendering when WebGL2 is unavailable or 3D rendering fails.
    • Automatically falls back to the 2D graph while preserving the saved mode preference.
    • Disabled the 3D toggle when unsupported, with an explanatory message.
    • Added recovery and retry handling for temporary graph rendering failures.
    • Prevented graph errors from disrupting the rest of the dashboard.
  • Tests

    • Added coverage for WebGL detection, fallback behavior, mode synchronization, toggle states, and renderer recovery.
    • Added end-to-end coverage for 2D fallback and supported 3D environments.

A browser without WebGL landing on any graph surface with a persisted
"3d" mode collapsed the whole app into the root error fallback: the
wrapper honoured localStorage unconditionally, three r163+ throws from
the WebGLRenderer constructor in a mount-time layout effect, and the
only boundary above it was the root one.
The wrapper now probes canvas.getContext("webgl2") before honouring a
persisted "3d" (ignoring, not rewriting, the preference), disables the
mode toggle with "3D requires WebGL" when the probe fails, and wraps
the graph slot in a mode-keyed local ErrorBoundary whose fallback
degrades a crashed 3D renderer to the 2D graph and heals the persisted
mode to "2d".
Unit tests were written first and watched fail (the crash test
reproduced the exact production throw escaping uncaught); the promoted
journey simulates a no-WebGL browser via an init-script getContext
override and failed against a pre-fix stack build with the dashboard on
the root fallback, exactly as reported.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 12, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-stagingd6cfaeaCommit Preview URL

Branch Preview URL
Aug 13 2026, 01:07 AM

@supabase

supabaseBot commented Aug 12, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: feb9f6c1-33ca-40db-986b-72181daea902

📥 Commits

Reviewing files that changed from the base of the PR and between ce7c291 and d6cfaea.

📒 Files selected for processing (11)
  • docs/frontend-testids.md
  • frontend/e2e/graph-webgl-fallback.spec.ts
  • frontend/eslint-suppressions.json
  • frontend/src/components/ErrorBoundary.tsx
  • frontend/src/components/graph/KnowledgeGraph.test.tsx
  • frontend/src/components/graph/KnowledgeGraph.tsx
  • frontend/src/components/graph/KnowledgeGraph3D.test.tsx
  • frontend/src/components/graph/KnowledgeGraph3D.testmode.test.tsx
  • frontend/src/components/graph/KnowledgeGraph3D.tsx
  • frontend/src/components/screens/AdminAnalytics.test.tsx
  • frontend/src/test-utils/mockNextDynamic.tsx

📝 Walkthrough

Walkthrough

KnowledgeGraph now detects WebGL2 before selecting 3D mode, contains renderer failures locally, preserves the stored preference, and adds unit and Playwright regression coverage.

Changes

Knowledge graph WebGL fallback

Layer / File(s)Summary
WebGL2 capability and mode gating
frontend/src/components/graph/KnowledgeGraph.tsx, frontend/src/components/graph/KnowledgeGraph3D.tsx
The graph probes WebGL2, derives an effective mode without changing the stored preference, and disables the 3D toggle when WebGL2 is unavailable.
Local renderer failure recovery
frontend/src/components/graph/KnowledgeGraph.tsx, frontend/src/components/ErrorBoundary.tsx, docs/frontend-testids.md
A graph-local boundary shows retryable fallback UI, re-probes after 3D failures, and switches to 2D when capability is lost.
Regression coverage and test infrastructure
frontend/src/components/graph/KnowledgeGraph.test.tsx, frontend/e2e/graph-webgl-fallback.spec.ts, frontend/src/test-utils/mockNextDynamic.tsx, frontend/src/components/graph/*test.tsx, frontend/src/components/screens/AdminAnalytics.test.tsx
Tests cover mode selection, WebGL2 gating, synchronization, toggle accessibility, crash recovery, retries, and persisted preferences. Shared dynamic-import mocking replaces local implementations.

Estimated code review effort: 4 (Complex) | ~45 minutes

Mergeability Score:⚪ Minimal · up to d6cfa

The change gates 3D rendering on WebGL2 and falls back to the 2D graph when renderer creation fails; no actionable merge-blocking risk remains after normal checks and review.

Sequence Diagram(s)

sequenceDiagram
participant Browser
participant KnowledgeGraph
participant localStorage
participant KnowledgeGraph3D
participant KnowledgeGraph2D
Browser->>KnowledgeGraph: mount graph
KnowledgeGraph->>Browser: probe WebGL2
Browser-->>KnowledgeGraph: return capability result
KnowledgeGraph->>localStorage: read persisted mode
KnowledgeGraph->>KnowledgeGraph2D: render effective 2D mode when WebGL2 is unavailable
KnowledgeGraph-->>Browser: expose disabled 3D toggle explanation
KnowledgeGraph->>KnowledgeGraph3D: mount 3D renderer when supported
KnowledgeGraph3D-->>KnowledgeGraph: report renderer failure
KnowledgeGraph->>KnowledgeGraph2D: render local 2D fallback
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 25.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly and concisely identifies the primary WebGL2 gating and renderer crash-containment changes.
Description check✅ PassedThe description explains the bug, implementation, linked issue, tests, and verification, although it does not follow every template heading.
Linked Issues check✅ PassedThe changes satisfy [#538] by gating persisted 3D mode, preserving preferences, containing graph crashes, and adding regression tests.
Out of Scope Changes check✅ PassedAll changes support the WebGL2 fallback, graph crash containment, accessibility, documentation, or related test infrastructure objectives.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/538-graph-webgl-fallback

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/src/components/graph/KnowledgeGraph.tsx`:
- Line 141: In frontend/src/components/graph/KnowledgeGraph.tsx:141-141, wrap
the fallback KnowledgeGraph2D rendered by GraphCrashFallback for crashedMode ===
"3d" in a nested ErrorBoundary using the inline status placeholder as its
fallback, preventing 2D failures from reaching the root fallback. In
frontend/src/components/graph/KnowledgeGraph.test.tsx:30-50, add the throw2d
mock control; in frontend/src/components/graph/KnowledgeGraph.test.tsx:148-176,
add a regression test covering simultaneous 3D and 2D failures and asserting the
nested placeholder is rendered.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5c77b3cc-2ebe-41aa-af76-de2ad1793ce2

📥 Commits

Reviewing files that changed from the base of the PR and between 7681c48 and ce7c291.

📒 Files selected for processing (3)
  • frontend/e2e/graph-webgl-fallback.spec.ts
  • frontend/src/components/graph/KnowledgeGraph.test.tsx
  • frontend/src/components/graph/KnowledgeGraph.tsx

React.useEffect(() => {
if (crashedMode === "3d") heal("2d");
}, [crashedMode, heal]);
if (crashedMode === "3d") return <KnowledgeGraph2D {...graphProps} />;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -e
printf'%s\n''--- KnowledgeGraph.tsx outline ---'
ast-grep outline frontend/src/components/graph/KnowledgeGraph.tsx
printf'%s\n''--- Relevant implementation ---'
sed -n '1,230p' frontend/src/components/graph/KnowledgeGraph.tsx
printf'%s\n''--- Relevant test structure ---'
sed -n '1,230p' frontend/src/components/graph/KnowledgeGraph.test.tsx

Repository: SaplingLearn/Sapling

Length of output: 14232


🏁 Script executed:

#!/bin/bashset -e
printf'%s\n''--- ErrorBoundary definition and usages ---'
fd -i 'ErrorBoundary' frontend/src
rg -n -C 12 'class ErrorBoundary|function ErrorBoundary|<ErrorBoundary' frontend/src
printf'%s\n''--- GraphCrashFallback references ---'
rg -n -C 8 'GraphCrashFallback|crashedMode === "3d"|The graph couldn' frontend/src/components/graph

Repository: SaplingLearn/Sapling

Length of output: 10536


🏁 Script executed:

#!/bin/bashset -e
cat -n frontend/src/components/ErrorBoundary.tsx
printf'%s\n''--- package and test scripts ---'if [ -f frontend/package.json ];then
sed -n '1,220p' frontend/package.json
fiif [ -f package.json ];then
sed -n '1,180p' package.json
fi

Repository: SaplingLearn/Sapling

Length of output: 7291


🏁 Script executed:

#!/bin/bashset -e
python3 - <<'PY'from pathlib import Pathimpl = Path("frontend/src/components/graph/KnowledgeGraph.tsx").read_text()boundary = Path("frontend/src/components/ErrorBoundary.tsx").read_text()test = Path("frontend/src/components/graph/KnowledgeGraph.test.tsx").read_text()checks = { "outer boundary invokes GraphCrashFallback as its fallback": ( '<GraphCrashFallback crashedMode={mode} graphProps={props} heal={setMode} />' in impl ), "3D fallback directly renders 2D": ( 'if (crashedMode === "3d") return <KnowledgeGraph2D {...graphProps} />;' in impl ), "ErrorBoundary calls fallback from its own render": ( "if (this.props.fallback) return this.props.fallback(this.state.error, this.reset);" in boundary ), "2D mock has no throw2d control": ( "throw2d" not in test ), "existing crash test only arms 3D": ( "mockState.throw3d = true;" in test and "mockState.throw2d" not in test ),}for name, result in checks.items(): print(f"{'PASS' if result else 'FAIL'}: {name}")# This models the relevant React error-boundary rule:# an error thrown while an ErrorBoundary renders its own fallback# cannot be handled by that same boundary.print("MODEL: 3D child throws -> outer boundary renders GraphCrashFallback")print("MODEL: GraphCrashFallback renders 2D directly -> a 2D throw occurs in outer fallback render")print("MODEL: without a nested boundary, that throw propagates to the next ancestor boundary")PY

Repository: SaplingLearn/Sapling

Length of output: 637


Contain a 2D failure during the 3D fallback.

When KnowledgeGraph3D throws, GraphCrashFallback renders KnowledgeGraph2D from the outer ErrorBoundary fallback. A 2D error then propagates to the root fallback. Wrap the fallback 2D graph in a nested ErrorBoundary with the inline status placeholder as its fallback. Add a throw2d mock control and regression test for simultaneous 3D and 2D failures.

📍 Affects 2 files
  • frontend/src/components/graph/KnowledgeGraph.tsx#L141-L141 (this comment)
  • frontend/src/components/graph/KnowledgeGraph.test.tsx#L30-L50
  • frontend/src/components/graph/KnowledgeGraph.test.tsx#L148-L176
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/graph/KnowledgeGraph.tsx` at line 141, In
frontend/src/components/graph/KnowledgeGraph.tsx:141-141, wrap the fallback
KnowledgeGraph2D rendered by GraphCrashFallback for crashedMode === "3d" in a
nested ErrorBoundary using the inline status placeholder as its fallback,
preventing 2D failures from reaching the root fallback. In
frontend/src/components/graph/KnowledgeGraph.test.tsx:30-50, add the throw2d
mock control; in frontend/src/components/graph/KnowledgeGraph.test.tsx:148-176,
add a regression test covering simultaneous 3D and 2D failures and asserting the
nested placeholder is rendered.

…led toggle, hardened tests
Reworks the crash containment so the persisted mode is never rewritten:
the wrapper derives an EFFECTIVE mode (one gate encoding) from the raw
persisted wish + a module-memoized WebGL2 probe, and a 3D crash re-probes
capability instead of healing localStorage — capability really gone flips
the effective mode (boundary re-keys, 2D mounts fresh); transient crashes
get a static placeholder with the boundary's reset wired to Try again.
The fallback never renders another graph component (a throw during a
boundary's own fallback render is uncatchable by that boundary), and
crashes now log in production.
The toggle swaps native disabled for aria-disabled + aria-describedby so
keyboard/SR users can reach the "3D requires WebGL" reason; the action
stays the accessible name.
Test hardening from the review: crash sentinels throw from a layout
effect (the real #538 crash phase); getContext is stubbed via vi.spyOn so
restoreAllMocks actually restores; storage-key/sync-event constants are
exported and imported by the unit tests (journey mirrors by comment);
SYNC_EVENT gating has direct coverage; the journey anchors its negative
assertions on the new error-fallback testid, re-asserts end state, and
pins the positive path (toggle enabled under real WebGL); the four
divergent next/dynamic passthrough mocks collapse into a shared
hook-safe helper.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Applied all 15 code-review findings in d6cfaea (design rework where the review asked for one, not point patches):

  • Crash containment redesigned (findings 1–5): the persisted mode is now never rewritten. The wrapper derives one EFFECTIVE mode from the raw persisted wish + a module-memoized WebGL2 probe; a 3D crash re-probes capability (the only signal the cached verdict may be stale) — capability really gone flips the effective mode and re-keys the boundary so 2D mounts fresh; transient crashes show a static placeholder with the boundary's reset wired to "Try again". The fallback never renders another graph component (a throw in a boundary's own fallback render is uncatchable by that boundary), and renderer crashes now console.error in production.
  • A11y (11): aria-disabled + aria-describedby replace native disabled; the toggle stays focusable, keeps its action name, and the "3D requires WebGL" reason is reachable.
  • Simplification (9, 10): the gate rule has one render-time encoding (effective = webglOk ? mode : "2d"); the probe is memoized with an explicit force re-probe path.
  • Test hardening (6, 7, 8, 12, 13, 14, 15): crash sentinels throw from a layout effect (the real crash phase); vi.spyOn getContext stub actually restores; exported storage-key/sync-event constants (journey mirrors by comment); direct SYNC_EVENT gating coverage; journey negatives anchor on the new error-fallback testid with an end-state re-assert; a real-browser positive-path test pins the toggle enabled under genuine WebGL2; the four divergent next/dynamic passthrough mocks collapsed into one shared hook-safe helper.

Verification: unit suite 627/627, eslint 0 errors (stale suppressions pruned), tsc clean, full e2e lane 44/44 (both #538 journeys) + oracles clean, run from a fresh worktree via one flock'd up→test→down cycle.

🤖 Generated with Claude Code

@AndresL230
AndresL230 merged commit 5c11b0c into mainAug 13, 2026
6 of 7 checks passed
@AndresL230
AndresL230 deleted the fix/538-graph-webgl-fallback branch August 13, 2026 01:07
Sign up for freeto 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.

Dashboard crashes to the root error fallback on browsers without WebGL when 3D graph mode is persisted

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); fix(graph): gate 3D mode on WebGL2 and contain renderer crashes by AndresL230 · Pull Request #539 · SaplingLearn/Sapling · GitHub
Skip to content

fix(graph): gate 3D mode on WebGL2 and contain renderer crashes - #539

Merged
AndresL230 merged 2 commits into
mainfrom
fix/538-graph-webgl-fallback
Aug 13, 2026
Merged

fix(graph): gate 3D mode on WebGL2 and contain renderer crashes#539
AndresL230 merged 2 commits into
mainfrom
fix/538-graph-webgl-fallback

Conversation

@AndresL230

@AndresL230AndresL230 commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Fixes#538.

The bug

A browser without WebGL that lands on the dashboard (or Tree/Learn) with localStorage["sapling.kg.mode"] = "3d" persisted collapses the entire app into the root error fallback ("We hit a snag"), permanently — "Try again" re-throws. Chain:

  1. The KnowledgeGraph wrapper honoured the persisted "3d" unconditionally — no WebGL capability check existed anywhere in the frontend.
  2. react-force-graph-3d mounts its engine synchronously in a useLayoutEffect (react-kapsule), constructing new three.WebGLRenderer(...).
  3. three.js r163+ throwsError creating WebGL context. from the constructor when canvas.getContext("webgl2") returns null.
  4. The nearest boundary was the root one (app/layout.tsx), so the whole tree unmounted.

A profile that toggled 3D on a WebGL-capable browser was locked out of the app the moment WebGL went away (disabled, GPU blocklist, remote desktop, VM). Fresh profiles were unaffected (mode defaults to 2D) — which is why this read as "used to work".

The fix (both in the wrapper, so Dashboard/Tree/Learn are all covered)

  1. WebGL2 capability gate (webgl2Available()): probe canvas.getContext("webgl2") once per wrapper mount (probe context released via WEBGL_lose_context so probes never count toward the per-page context cap). Without WebGL2:
    • a persisted "3d" is ignored, not rewritten — the preference survives for the profile's WebGL-capable browsers;
    • the mode toggle renders disabled with title/aria-label "3D requires WebGL";
    • the storage/custom-event mode-sync paths and the setter are gated identically.
  2. Graph-local error boundary, keyed by mode: any residual renderer crash (context creation failing despite a successful probe — GPU process crash, context-limit exhaustion) degrades to the 2D graph inline and heals the persisted mode to "2d"; the heal flips the boundary key, clearing the error state into a normal 2D render. A (never-observed) 2D crash renders a minimal placeholder instead of remounting the thing that just threw.

Tests

  • Unit (KnowledgeGraph.test.tsx, 6 tests, written first and watched fail): fresh-profile default pin; 3d-honoured-with-WebGL pin; forces-2D-without-WebGL2; toggle disabled with explanatory label; toggle enabled with WebGL; crash containment + mode heal (RED run reproduced the exact production throw escaping uncaught).
  • Promoted journey (e2e/graph-webgl-fallback.spec.ts): init script nulls the webgl/webgl2 branches of getContext (exactly the API three probes; 2D canvas stays real) and seeds the poisoned "3d" mode → dashboard must render the 2D SVG graph, no root fallback, toggle disabled, preference preserved.

Verification

  • vitest run: 72 files / 624+ tests green (against a jsdom that is itself a no-WebGL environment, so every existing screen test now exercises the gated path).
  • tsc --noEmit clean; eslint clean on changed files (no new suppressions).
  • next build clean.
  • Pre-fix RED run (stack cycle 1): with the wrapper fix stashed and the real production build served, the new journey fails — the dashboard collapses into the root fallback exactly as reported. The journey failed at the "We hit a snag" count-0 assertion (exit 1) — the root fallback was live on the page.
  • Post-fix full lane (stack cycle 2): entire Chapter-1 Playwright lane + deterministic oracles green. 43 passed (1.5m), oracles exit 0.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved graph rendering when WebGL2 is unavailable or 3D rendering fails.
    • Automatically falls back to the 2D graph while preserving the saved mode preference.
    • Disabled the 3D toggle when unsupported, with an explanatory message.
    • Added recovery and retry handling for temporary graph rendering failures.
    • Prevented graph errors from disrupting the rest of the dashboard.
  • Tests

    • Added coverage for WebGL detection, fallback behavior, mode synchronization, toggle states, and renderer recovery.
    • Added end-to-end coverage for 2D fallback and supported 3D environments.

A browser without WebGL landing on any graph surface with a persisted
"3d" mode collapsed the whole app into the root error fallback: the
wrapper honoured localStorage unconditionally, three r163+ throws from
the WebGLRenderer constructor in a mount-time layout effect, and the
only boundary above it was the root one.
The wrapper now probes canvas.getContext("webgl2") before honouring a
persisted "3d" (ignoring, not rewriting, the preference), disables the
mode toggle with "3D requires WebGL" when the probe fails, and wraps
the graph slot in a mode-keyed local ErrorBoundary whose fallback
degrades a crashed 3D renderer to the 2D graph and heals the persisted
mode to "2d".
Unit tests were written first and watched fail (the crash test
reproduced the exact production throw escaping uncaught); the promoted
journey simulates a no-WebGL browser via an init-script getContext
override and failed against a pre-fix stack build with the dashboard on
the root fallback, exactly as reported.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 12, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-stagingd6cfaeaCommit Preview URL

Branch Preview URL
Aug 13 2026, 01:07 AM

@supabase

supabaseBot commented Aug 12, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: feb9f6c1-33ca-40db-986b-72181daea902

📥 Commits

Reviewing files that changed from the base of the PR and between ce7c291 and d6cfaea.

📒 Files selected for processing (11)
  • docs/frontend-testids.md
  • frontend/e2e/graph-webgl-fallback.spec.ts
  • frontend/eslint-suppressions.json
  • frontend/src/components/ErrorBoundary.tsx
  • frontend/src/components/graph/KnowledgeGraph.test.tsx
  • frontend/src/components/graph/KnowledgeGraph.tsx
  • frontend/src/components/graph/KnowledgeGraph3D.test.tsx
  • frontend/src/components/graph/KnowledgeGraph3D.testmode.test.tsx
  • frontend/src/components/graph/KnowledgeGraph3D.tsx
  • frontend/src/components/screens/AdminAnalytics.test.tsx
  • frontend/src/test-utils/mockNextDynamic.tsx

📝 Walkthrough

Walkthrough

KnowledgeGraph now detects WebGL2 before selecting 3D mode, contains renderer failures locally, preserves the stored preference, and adds unit and Playwright regression coverage.

Changes

Knowledge graph WebGL fallback

Layer / File(s)Summary
WebGL2 capability and mode gating
frontend/src/components/graph/KnowledgeGraph.tsx, frontend/src/components/graph/KnowledgeGraph3D.tsx
The graph probes WebGL2, derives an effective mode without changing the stored preference, and disables the 3D toggle when WebGL2 is unavailable.
Local renderer failure recovery
frontend/src/components/graph/KnowledgeGraph.tsx, frontend/src/components/ErrorBoundary.tsx, docs/frontend-testids.md
A graph-local boundary shows retryable fallback UI, re-probes after 3D failures, and switches to 2D when capability is lost.
Regression coverage and test infrastructure
frontend/src/components/graph/KnowledgeGraph.test.tsx, frontend/e2e/graph-webgl-fallback.spec.ts, frontend/src/test-utils/mockNextDynamic.tsx, frontend/src/components/graph/*test.tsx, frontend/src/components/screens/AdminAnalytics.test.tsx
Tests cover mode selection, WebGL2 gating, synchronization, toggle accessibility, crash recovery, retries, and persisted preferences. Shared dynamic-import mocking replaces local implementations.

Estimated code review effort: 4 (Complex) | ~45 minutes

Mergeability Score:⚪ Minimal · up to d6cfa

The change gates 3D rendering on WebGL2 and falls back to the 2D graph when renderer creation fails; no actionable merge-blocking risk remains after normal checks and review.

Sequence Diagram(s)

sequenceDiagram
participant Browser
participant KnowledgeGraph
participant localStorage
participant KnowledgeGraph3D
participant KnowledgeGraph2D
Browser->>KnowledgeGraph: mount graph
KnowledgeGraph->>Browser: probe WebGL2
Browser-->>KnowledgeGraph: return capability result
KnowledgeGraph->>localStorage: read persisted mode
KnowledgeGraph->>KnowledgeGraph2D: render effective 2D mode when WebGL2 is unavailable
KnowledgeGraph-->>Browser: expose disabled 3D toggle explanation
KnowledgeGraph->>KnowledgeGraph3D: mount 3D renderer when supported
KnowledgeGraph3D-->>KnowledgeGraph: report renderer failure
KnowledgeGraph->>KnowledgeGraph2D: render local 2D fallback
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 25.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly and concisely identifies the primary WebGL2 gating and renderer crash-containment changes.
Description check✅ PassedThe description explains the bug, implementation, linked issue, tests, and verification, although it does not follow every template heading.
Linked Issues check✅ PassedThe changes satisfy [#538] by gating persisted 3D mode, preserving preferences, containing graph crashes, and adding regression tests.
Out of Scope Changes check✅ PassedAll changes support the WebGL2 fallback, graph crash containment, accessibility, documentation, or related test infrastructure objectives.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/538-graph-webgl-fallback

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/src/components/graph/KnowledgeGraph.tsx`:
- Line 141: In frontend/src/components/graph/KnowledgeGraph.tsx:141-141, wrap
the fallback KnowledgeGraph2D rendered by GraphCrashFallback for crashedMode ===
"3d" in a nested ErrorBoundary using the inline status placeholder as its
fallback, preventing 2D failures from reaching the root fallback. In
frontend/src/components/graph/KnowledgeGraph.test.tsx:30-50, add the throw2d
mock control; in frontend/src/components/graph/KnowledgeGraph.test.tsx:148-176,
add a regression test covering simultaneous 3D and 2D failures and asserting the
nested placeholder is rendered.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5c77b3cc-2ebe-41aa-af76-de2ad1793ce2

📥 Commits

Reviewing files that changed from the base of the PR and between 7681c48 and ce7c291.

📒 Files selected for processing (3)
  • frontend/e2e/graph-webgl-fallback.spec.ts
  • frontend/src/components/graph/KnowledgeGraph.test.tsx
  • frontend/src/components/graph/KnowledgeGraph.tsx

React.useEffect(() => {
if (crashedMode === "3d") heal("2d");
}, [crashedMode, heal]);
if (crashedMode === "3d") return <KnowledgeGraph2D {...graphProps} />;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -e
printf'%s\n''--- KnowledgeGraph.tsx outline ---'
ast-grep outline frontend/src/components/graph/KnowledgeGraph.tsx
printf'%s\n''--- Relevant implementation ---'
sed -n '1,230p' frontend/src/components/graph/KnowledgeGraph.tsx
printf'%s\n''--- Relevant test structure ---'
sed -n '1,230p' frontend/src/components/graph/KnowledgeGraph.test.tsx

Repository: SaplingLearn/Sapling

Length of output: 14232


🏁 Script executed:

#!/bin/bashset -e
printf'%s\n''--- ErrorBoundary definition and usages ---'
fd -i 'ErrorBoundary' frontend/src
rg -n -C 12 'class ErrorBoundary|function ErrorBoundary|<ErrorBoundary' frontend/src
printf'%s\n''--- GraphCrashFallback references ---'
rg -n -C 8 'GraphCrashFallback|crashedMode === "3d"|The graph couldn' frontend/src/components/graph

Repository: SaplingLearn/Sapling

Length of output: 10536


🏁 Script executed:

#!/bin/bashset -e
cat -n frontend/src/components/ErrorBoundary.tsx
printf'%s\n''--- package and test scripts ---'if [ -f frontend/package.json ];then
sed -n '1,220p' frontend/package.json
fiif [ -f package.json ];then
sed -n '1,180p' package.json
fi

Repository: SaplingLearn/Sapling

Length of output: 7291


🏁 Script executed:

#!/bin/bashset -e
python3 - <<'PY'from pathlib import Pathimpl = Path("frontend/src/components/graph/KnowledgeGraph.tsx").read_text()boundary = Path("frontend/src/components/ErrorBoundary.tsx").read_text()test = Path("frontend/src/components/graph/KnowledgeGraph.test.tsx").read_text()checks = { "outer boundary invokes GraphCrashFallback as its fallback": ( '<GraphCrashFallback crashedMode={mode} graphProps={props} heal={setMode} />' in impl ), "3D fallback directly renders 2D": ( 'if (crashedMode === "3d") return <KnowledgeGraph2D {...graphProps} />;' in impl ), "ErrorBoundary calls fallback from its own render": ( "if (this.props.fallback) return this.props.fallback(this.state.error, this.reset);" in boundary ), "2D mock has no throw2d control": ( "throw2d" not in test ), "existing crash test only arms 3D": ( "mockState.throw3d = true;" in test and "mockState.throw2d" not in test ),}for name, result in checks.items(): print(f"{'PASS' if result else 'FAIL'}: {name}")# This models the relevant React error-boundary rule:# an error thrown while an ErrorBoundary renders its own fallback# cannot be handled by that same boundary.print("MODEL: 3D child throws -> outer boundary renders GraphCrashFallback")print("MODEL: GraphCrashFallback renders 2D directly -> a 2D throw occurs in outer fallback render")print("MODEL: without a nested boundary, that throw propagates to the next ancestor boundary")PY

Repository: SaplingLearn/Sapling

Length of output: 637


Contain a 2D failure during the 3D fallback.

When KnowledgeGraph3D throws, GraphCrashFallback renders KnowledgeGraph2D from the outer ErrorBoundary fallback. A 2D error then propagates to the root fallback. Wrap the fallback 2D graph in a nested ErrorBoundary with the inline status placeholder as its fallback. Add a throw2d mock control and regression test for simultaneous 3D and 2D failures.

📍 Affects 2 files
  • frontend/src/components/graph/KnowledgeGraph.tsx#L141-L141 (this comment)
  • frontend/src/components/graph/KnowledgeGraph.test.tsx#L30-L50
  • frontend/src/components/graph/KnowledgeGraph.test.tsx#L148-L176
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/graph/KnowledgeGraph.tsx` at line 141, In
frontend/src/components/graph/KnowledgeGraph.tsx:141-141, wrap the fallback
KnowledgeGraph2D rendered by GraphCrashFallback for crashedMode === "3d" in a
nested ErrorBoundary using the inline status placeholder as its fallback,
preventing 2D failures from reaching the root fallback. In
frontend/src/components/graph/KnowledgeGraph.test.tsx:30-50, add the throw2d
mock control; in frontend/src/components/graph/KnowledgeGraph.test.tsx:148-176,
add a regression test covering simultaneous 3D and 2D failures and asserting the
nested placeholder is rendered.

…led toggle, hardened tests
Reworks the crash containment so the persisted mode is never rewritten:
the wrapper derives an EFFECTIVE mode (one gate encoding) from the raw
persisted wish + a module-memoized WebGL2 probe, and a 3D crash re-probes
capability instead of healing localStorage — capability really gone flips
the effective mode (boundary re-keys, 2D mounts fresh); transient crashes
get a static placeholder with the boundary's reset wired to Try again.
The fallback never renders another graph component (a throw during a
boundary's own fallback render is uncatchable by that boundary), and
crashes now log in production.
The toggle swaps native disabled for aria-disabled + aria-describedby so
keyboard/SR users can reach the "3D requires WebGL" reason; the action
stays the accessible name.
Test hardening from the review: crash sentinels throw from a layout
effect (the real #538 crash phase); getContext is stubbed via vi.spyOn so
restoreAllMocks actually restores; storage-key/sync-event constants are
exported and imported by the unit tests (journey mirrors by comment);
SYNC_EVENT gating has direct coverage; the journey anchors its negative
assertions on the new error-fallback testid, re-asserts end state, and
pins the positive path (toggle enabled under real WebGL); the four
divergent next/dynamic passthrough mocks collapse into a shared
hook-safe helper.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Applied all 15 code-review findings in d6cfaea (design rework where the review asked for one, not point patches):

  • Crash containment redesigned (findings 1–5): the persisted mode is now never rewritten. The wrapper derives one EFFECTIVE mode from the raw persisted wish + a module-memoized WebGL2 probe; a 3D crash re-probes capability (the only signal the cached verdict may be stale) — capability really gone flips the effective mode and re-keys the boundary so 2D mounts fresh; transient crashes show a static placeholder with the boundary's reset wired to "Try again". The fallback never renders another graph component (a throw in a boundary's own fallback render is uncatchable by that boundary), and renderer crashes now console.error in production.
  • A11y (11): aria-disabled + aria-describedby replace native disabled; the toggle stays focusable, keeps its action name, and the "3D requires WebGL" reason is reachable.
  • Simplification (9, 10): the gate rule has one render-time encoding (effective = webglOk ? mode : "2d"); the probe is memoized with an explicit force re-probe path.
  • Test hardening (6, 7, 8, 12, 13, 14, 15): crash sentinels throw from a layout effect (the real crash phase); vi.spyOn getContext stub actually restores; exported storage-key/sync-event constants (journey mirrors by comment); direct SYNC_EVENT gating coverage; journey negatives anchor on the new error-fallback testid with an end-state re-assert; a real-browser positive-path test pins the toggle enabled under genuine WebGL2; the four divergent next/dynamic passthrough mocks collapsed into one shared hook-safe helper.

Verification: unit suite 627/627, eslint 0 errors (stale suppressions pruned), tsc clean, full e2e lane 44/44 (both #538 journeys) + oracles clean, run from a fresh worktree via one flock'd up→test→down cycle.

🤖 Generated with Claude Code

@AndresL230
AndresL230 merged commit 5c11b0c into mainAug 13, 2026
6 of 7 checks passed
@AndresL230
AndresL230 deleted the fix/538-graph-webgl-fallback branch August 13, 2026 01:07
Sign up for freeto 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.

Dashboard crashes to the root error fallback on browsers without WebGL when 3D graph mode is persisted

1 participant

@AndresL230