mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 14:55:43 +00:00
feat(app): top-level error boundary so a render error no longer blanks the app (#1194)
* feat(app): add a top-level error boundary so a render error no longer blanks the app Until now any thrown error during render took the whole window down with no recovery (issue #382). A class ErrorBoundary around the authenticated shell catches it and shows a recoverable fallback (Try again / Reload app) while playback keeps going. Hook-free + English-only with literal CSS var() fallbacks so it renders even when i18n/theme/state is what broke. * docs(changelog): error boundary prevents the app blanking on a render error (PR #1194)
This commit is contained in:
@@ -337,6 +337,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
* The AutoDJ overlap-cap and the Native Hi-Res blend-rate options in Settings → Audio now sit in the same bordered sub-card the Normalization options use, and the Hi-Res section no longer shows a double border.
|
||||
|
||||
### App no longer blanks on an unexpected error
|
||||
|
||||
**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#1194](https://github.com/Psychotoxical/psysonic/pull/1194)**
|
||||
|
||||
* If a screen hit an unexpected rendering error, the whole window could go blank with no way back. The app now shows a small recoverable error card (Try again / Reload app) instead, and playback keeps going.
|
||||
|
||||
## [1.48.1] - 2026-06-15
|
||||
|
||||
## Fixed
|
||||
|
||||
+6
-3
@@ -32,6 +32,7 @@ import { useMigrationOrchestrator } from '../hooks/useMigrationOrchestrator';
|
||||
import { IS_WINDOWS } from '../utils/platform';
|
||||
import TauriEventBridge from './TauriEventBridge';
|
||||
import AppShell from './AppShell';
|
||||
import ErrorBoundary from '../components/ErrorBoundary';
|
||||
import BlockingMigrationGate from './BlockingMigrationGate';
|
||||
import RequireAuth from './RequireAuth';
|
||||
import { useMigrationStore } from '../store/migrationStore';
|
||||
@@ -191,9 +192,11 @@ export default function MainApp() {
|
||||
path="/*"
|
||||
element={
|
||||
<RequireAuth>
|
||||
<DragDropProvider>
|
||||
<AppShell />
|
||||
</DragDropProvider>
|
||||
<ErrorBoundary>
|
||||
<DragDropProvider>
|
||||
<AppShell />
|
||||
</DragDropProvider>
|
||||
</ErrorBoundary>
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Component, type ErrorInfo, type ReactNode } from 'react';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
interface State {
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* App-level error boundary. A render-time throw used to blank the entire window
|
||||
* — no boundary existed (issue #382), so any thrown error during render took the
|
||||
* whole UI down. This catches it and shows a recoverable fallback while playback
|
||||
* (driven by the Rust audio engine, outside React) keeps going.
|
||||
*
|
||||
* Deliberately hook-free and English-only: the fallback has to render even when
|
||||
* i18n, the theme, or app state is exactly what broke, so it must not depend on
|
||||
* any of them. The CSS uses literal fallbacks in `var(...)` for the same reason.
|
||||
*/
|
||||
export default class ErrorBoundary extends Component<Props, State> {
|
||||
state: State = { error: null };
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return { error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, info: ErrorInfo): void {
|
||||
console.error('[error-boundary]', error, info.componentStack);
|
||||
}
|
||||
|
||||
private handleRetry = (): void => this.setState({ error: null });
|
||||
|
||||
private handleReload = (): void => window.location.reload();
|
||||
|
||||
render(): ReactNode {
|
||||
const { error } = this.state;
|
||||
if (!error) return this.props.children;
|
||||
return (
|
||||
<div className="app-error-boundary" role="alert">
|
||||
<div className="app-error-boundary__card">
|
||||
<h1 className="app-error-boundary__title">Something went wrong</h1>
|
||||
<p className="app-error-boundary__text">
|
||||
This view hit an unexpected error and stopped rendering. Playback keeps going — try the
|
||||
view again, or reload the app.
|
||||
</p>
|
||||
<pre className="app-error-boundary__detail">{error.message}</pre>
|
||||
<div className="app-error-boundary__actions">
|
||||
<button type="button" className="btn btn-primary" onClick={this.handleRetry}>
|
||||
Try again
|
||||
</button>
|
||||
<button type="button" className="btn btn-surface" onClick={this.handleReload}>
|
||||
Reload app
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/* App-level error boundary fallback (components/ErrorBoundary.tsx). Literal
|
||||
var() fallbacks so it renders even when the active theme is what broke. */
|
||||
.app-error-boundary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 60vh;
|
||||
padding: var(--space-6, 24px);
|
||||
background: var(--bg-app, #1e1e2e);
|
||||
color: var(--text-primary, #cdd6f4);
|
||||
}
|
||||
|
||||
.app-error-boundary__card {
|
||||
width: 100%;
|
||||
max-width: 520px;
|
||||
padding: var(--space-6, 24px);
|
||||
background: var(--bg-card, #313244);
|
||||
border: 1px solid var(--border, #45475a);
|
||||
border-radius: var(--radius-lg, 12px);
|
||||
box-shadow: var(--shadow-md, 0 8px 24px rgba(0, 0, 0, 0.3));
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.app-error-boundary__title {
|
||||
margin: 0 0 var(--space-2, 8px);
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.app-error-boundary__text {
|
||||
margin: 0 0 var(--space-4, 16px);
|
||||
color: var(--text-secondary, #bac2de);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.app-error-boundary__detail {
|
||||
margin: 0 0 var(--space-4, 16px);
|
||||
padding: var(--space-3, 12px);
|
||||
max-height: 160px;
|
||||
overflow: auto;
|
||||
background: var(--bg-deep, #181825);
|
||||
border-radius: var(--radius-md, 8px);
|
||||
color: var(--text-muted, #a6adc8);
|
||||
font-size: 12px;
|
||||
text-align: left;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.app-error-boundary__actions {
|
||||
display: flex;
|
||||
gap: var(--space-3, 12px);
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -94,3 +94,4 @@
|
||||
@import './np-dash.css';
|
||||
@import './orbit-session-top-strip.css';
|
||||
@import './back-to-top.css';
|
||||
@import './error-boundary.css';
|
||||
|
||||
Reference in New Issue
Block a user