Fix the blank VPN tab, and stop one page taking the console with it
On a machine with wireguard-tools installed but no tunnel up — core, which is the machine the console opens on — the tunnel list was a nil slice, which is null in JSON, and the page took its length. That throws, React unmounts the whole tree, and what is left is a blank window with nothing to go on. Two fixes, because either alone leaves the failure possible. The parse returns an empty list rather than nil, with a test that pins the marshalled shape; and the page treats a missing list as an empty one, so a node still running an older build cannot blank the console either. And the reason a small mistake had such a large blast radius: nothing caught it. Each page now renders inside a boundary, so a fault costs that page and says what it was, while the rail, the switcher and every other tab carry on. The terminals are deliberately left outside it — they hold live WebSockets, and remounting them to recover from somebody else's error would drop the sessions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
acb83d3a4a
commit
32fdbf169a
+32
-1
@@ -1,4 +1,4 @@
|
||||
import { ReactNode, useEffect, useRef, useState } from 'react'
|
||||
import { Component, ReactNode, useEffect, useRef, useState } from 'react'
|
||||
|
||||
// ---- spinners ----
|
||||
export function Spinner({ size = 14 }: { size?: number }) {
|
||||
@@ -155,3 +155,34 @@ export async function okToLeave(): Promise<boolean> {
|
||||
if (!unsaved || (await askYes(`Discard unsaved changes to ${unsaved}?`, 'Discard'))) { unsaved = ''; return true }
|
||||
return false
|
||||
}
|
||||
|
||||
// A page that throws takes the whole console down with it: React unmounts the tree, and what
|
||||
// is left is a blank window with no clue in it. The VPN tab did exactly that on a machine
|
||||
// whose tunnel list came back null. So each page is rendered inside one of these, and a fault
|
||||
// costs that page rather than the session — the rail, the switcher and every other tab still
|
||||
// work, and the error says which page and what it said.
|
||||
export class PageBoundary extends Component<{ page: string; children: ReactNode }, { err: Error | null }> {
|
||||
constructor(props: { page: string; children: ReactNode }) {
|
||||
super(props)
|
||||
this.state = { err: null }
|
||||
}
|
||||
static getDerivedStateFromError(err: Error) { return { err } }
|
||||
componentDidCatch(err: Error) { console.error('page failed:', err) }
|
||||
// A different page is a different mount; clear the fault when the page changes.
|
||||
componentDidUpdate(prev: { page: string }) { if (prev.page !== this.props.page && this.state.err) this.setState({ err: null }) }
|
||||
render() {
|
||||
if (!this.state.err) return this.props.children
|
||||
return (
|
||||
<div className="dash">
|
||||
<div className="dash-bar"><h2 className="section">{this.props.page}</h2></div>
|
||||
<section className="card">
|
||||
<div className="error">This page hit a fault and stopped drawing. The rest of the console is unaffected.</div>
|
||||
<div className="muted small mono" style={{ marginTop: 8 }}>{String(this.state.err)}</div>
|
||||
<div className="svc-actions" style={{ marginTop: 10 }}>
|
||||
<button className="btn" onClick={() => this.setState({ err: null })}>Try again</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user