diff --git a/EngineDesign/frontend/src/App.tsx b/EngineDesign/frontend/src/App.tsx index a509dd39..2feac84d 100644 --- a/EngineDesign/frontend/src/App.tsx +++ b/EngineDesign/frontend/src/App.tsx @@ -13,6 +13,7 @@ import ConfigurationSelector from './components/ConfigurationSelector'; import { emitConfigChanged } from './lib/configBus'; import { useViewState } from './lib/viewState'; import { DesignVersions } from './components/DesignVersions'; +import { ErrorBoundary } from './components/ErrorBoundary'; import { ReadOnlyProvider } from '@stardesign-ui'; import { getConfig, getHealth } from './api/client'; import type { EngineConfig } from './api/client'; @@ -237,117 +238,135 @@ function App() { {/* Keep all tab panels mounted; hide inactive ones to preserve state */}
-
- {!config && ( -
-

Load Configuration

- -
- )} - -
+ +
+ {!config && ( +
+

Load Configuration

+ +
+ )} + +
+
-
- {!config && ( -
-

Load Configuration

- -
- )} - -
+ +
+ {!config && ( +
+

Load Configuration

+ +
+ )} + +
+
- + + +
-
- {!config && ( -
-

Load Configuration

- -
- )} - -
+ +
+ {!config && ( +
+

Load Configuration

+ +
+ )} + +
+
-
- {!config && ( -
-

Load Configuration

- -
- )} - -
+ +
+ {!config && ( +
+

Load Configuration

+ +
+ )} + +
+
-
- {!config && ( -
-

Load Configuration

- -
- )} - -
+ +
+ {!config && ( +
+

Load Configuration

+ +
+ )} + +
+
-
- {!config && ( -
-

Load Configuration

- -
- )} - -
+ +
+ {!config && ( +
+

Load Configuration

+ +
+ )} + +
+
-
- {!config && ( -
-

Load Configuration

- -
- )} - -
+ +
+ {!config && ( +
+

Load Configuration

+ +
+ )} + +
+
-
- {/* Upload section - compact */} -
-
-
- -
- {config && ( -
- - - - Config loaded and ready + +
+ {/* Upload section - compact */} +
+
+
+
- )} + {config && ( +
+ + + + Config loaded and ready +
+ )} +
-
- {/* Editor section - full width */} -
- + {/* Editor section - full width */} +
+ +
-
+
diff --git a/EngineDesign/frontend/src/components/ErrorBoundary.tsx b/EngineDesign/frontend/src/components/ErrorBoundary.tsx new file mode 100644 index 00000000..d9e2e1d3 --- /dev/null +++ b/EngineDesign/frontend/src/components/ErrorBoundary.tsx @@ -0,0 +1,75 @@ +import { Component, type ErrorInfo, type ReactNode } from 'react'; + +/** + * Catches render-time exceptions so one bad value cannot blank the whole app. + * + * Without a boundary anywhere in the tree, React unmounts EVERYTHING when a + * render throws -- the user sees a white page with no message, no stack, and no + * way to report what happened. Every render bug then looks identical, which is + * exactly how "press Optimize, page goes blank" got reported with nothing to go + * on. This keeps the failure on screen and legible instead. + */ +interface Props { + children: ReactNode; + /** Shown above the error, e.g. "Flight Simulation". */ + label?: string; +} +interface State { + error: Error | null; + info: ErrorInfo | null; +} + +export class ErrorBoundary extends Component { + state: State = { error: null, info: null }; + + static getDerivedStateFromError(error: Error): Partial { + return { error }; + } + + componentDidCatch(error: Error, info: ErrorInfo) { + // Keep the console record: the boundary stops the crash from propagating, + // so without this the stack would be swallowed entirely. + console.error('[ErrorBoundary]', this.props.label ?? '', error, info.componentStack); + this.setState({ info }); + } + + private reset = () => this.setState({ error: null, info: null }); + + render() { + const { error, info } = this.state; + if (!error) return this.props.children; + + const detail = [error.stack || String(error), info?.componentStack] + .filter(Boolean) + .join('\n\nComponent stack:'); + + return ( +
+

+ {this.props.label ? `${this.props.label} hit an error` : 'Something went wrong'} +

+

+ The rest of the app is still running. Copy the detail below when reporting this. +

+

{String(error.message || error)}

+
+ + Show stack + +
+            {detail}
+          
+
+ +
+ ); + } +} + +export default ErrorBoundary; diff --git a/EngineDesign/frontend/src/lib/gating.test.ts b/EngineDesign/frontend/src/lib/gating.test.ts index 4aa7f619..74474c97 100644 --- a/EngineDesign/frontend/src/lib/gating.test.ts +++ b/EngineDesign/frontend/src/lib/gating.test.ts @@ -79,6 +79,7 @@ const VIEW_ONLY: Record = { 'ConfigEditor.tsx:setSearchQuery': 'filters which sections are shown', 'ConfigEditor.tsx:setIsExpanded': 'expand/collapse a section', 'ConfigUpload.tsx:label': 'the drop zone wrapper, not a control', + 'ErrorBoundary.tsx:this.reset': 'clears a caught render error; touches no design state', 'Layer1Optimization.tsx:setShowParameterPlots': 'chart visibility', 'Layer1Optimization.tsx:setShowInjectorPressures': 'chart visibility', 'Layer1Optimization.tsx:setShowSolverInputsEcho': 'diagnostics visibility',