feat(cluster): GUI-Repair-Button für Config-Drift + Stale-Chunk-Auto-Reload — v1.2.86
Cluster/Replication:
- Drift-Banner: Button 'Resync erzwingen' baut die PG-Logical-Replication-
Subscription neu auf (via edgeguard-ctl cluster-setup-standby).
- Primary-Dispatch: Button auf dem Primary delegiert per mTLS an den
Standby (POST /agent/cluster/repair-replication); auf dem Standby lokal.
- Status-Proxy Primary->Standby via Aggregator.FanOut; Erfolg = Job-success
ODER drift_found wird false (--collect-Unit verschwindet nach Erfolg).
- Job als transiente systemd-Unit edgeguard-repair-replication.service
(sudoers exact-match + festes Script wie upgrade.sh).
- Banner-Text korrigiert (keine 'Outbox').
Frontend-Stabilität:
- Stale-Chunk-Auto-Reload: Lazy-Import-Fehler nach Deploy ('Failed to fetch
dynamically imported module') lösen einen einmaligen Reload aus (Loop-
Schutz via sessionStorage) statt einer Fehlerseite. Globaler
vite:preloadError-Listener + ErrorBoundary-Integration.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,35 +1,65 @@
|
||||
import { Component, type ErrorInfo, type ReactNode } from 'react'
|
||||
|
||||
// Top-level ErrorBoundary. Catches throws aus dem React-Tree (inkl.
|
||||
// Lazy-Chunk-Loadfehler, die auf flakigem Mobilfunk häufig sind) und
|
||||
// rendert eine sichtbare Fehlerseite statt #root leer zu lassen.
|
||||
// Ohne diese Boundary endet jeder Render-Throw als „blank page".
|
||||
//
|
||||
// Wir loggen den Fehler in die Browser-Console (für Remote-Debug via
|
||||
// Safari-Inspector/Chrome-Remote) und zeigen dem Operator die
|
||||
// Fehlermeldung wörtlich — kein Translation-Layer, weil i18n selbst
|
||||
// schon kaputt sein kann.
|
||||
import { isStaleChunkError, reloadForStaleChunkOnce } from '../lib/staleChunkReload'
|
||||
|
||||
interface State { error: Error | null }
|
||||
// Top-level ErrorBoundary. Catches throws aus dem React-Tree (inkl.
|
||||
// Lazy-Chunk-Loadfehler nach einem Deploy) und rendert eine sichtbare
|
||||
// Fehlerseite statt #root leer zu lassen. Ohne diese Boundary endet
|
||||
// jeder Render-Throw als „blank page".
|
||||
//
|
||||
// Stale-Chunk-Fehler (alter Tab referenziert nicht mehr existierende
|
||||
// gehashte Chunks nach einem Deploy) werden automatisch per einmaligem
|
||||
// Reload behoben — der Operator sieht dann nur kurz „Aktualisiere…".
|
||||
// Erst wenn auch der Reload nicht hilft (giveUp) zeigen wir die manuelle
|
||||
// Fehlerkarte. Andere Fehler werden wörtlich angezeigt — kein
|
||||
// Translation-Layer, weil i18n selbst kaputt sein kann.
|
||||
|
||||
interface State { error: Error | null; giveUp: boolean }
|
||||
|
||||
export default class ErrorBoundary extends Component<{ children: ReactNode }, State> {
|
||||
state: State = { error: null }
|
||||
state: State = { error: null, giveUp: false }
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
static getDerivedStateFromError(error: Error): Partial<State> {
|
||||
return { error }
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, info: ErrorInfo) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('[ErrorBoundary]', error, info.componentStack)
|
||||
// Stale-Chunk → einmalig neu laden. Schlägt der Loop-Schutz an
|
||||
// (Reload half nicht), auf die manuelle Karte zurückfallen.
|
||||
if (isStaleChunkError(error) && !reloadForStaleChunkOnce()) {
|
||||
this.setState({ giveUp: true })
|
||||
}
|
||||
}
|
||||
|
||||
reset = () => { this.setState({ error: null }) }
|
||||
reset = () => { this.setState({ error: null, giveUp: false }) }
|
||||
|
||||
render() {
|
||||
const err = this.state.error
|
||||
if (!err) return this.props.children
|
||||
const isChunkErr = /Loading chunk|Failed to fetch dynamically imported module|Importing a module script failed/i.test(err.message)
|
||||
const isChunkErr = isStaleChunkError(err)
|
||||
|
||||
// Auto-Reload läuft (Chunk-Fehler, Loop-Schutz noch nicht erreicht):
|
||||
// neutralen Lade-Hinweis zeigen statt der Fehlerkarte.
|
||||
if (isChunkErr && !this.state.giveUp) {
|
||||
return (
|
||||
<div style={{
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 24,
|
||||
background: '#F8FAFC',
|
||||
color: '#64748B',
|
||||
fontSize: 14,
|
||||
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
||||
}}>
|
||||
Aktualisiere EdgeGuard…
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
minHeight: '100vh',
|
||||
@@ -54,7 +84,7 @@ export default class ErrorBoundary extends Component<{ children: ReactNode }, St
|
||||
</div>
|
||||
<div style={{ fontSize: 13, color: '#64748B', marginBottom: 16 }}>
|
||||
{isChunkErr
|
||||
? 'Ein Teil der App konnte nicht aus dem Netz geladen werden. Das passiert häufig bei wechselndem Mobilfunk-Empfang. Versuche es mit einem Reload.'
|
||||
? 'Ein Teil der App konnte nicht geladen werden — auch ein automatischer Reload hat nicht geholfen. Bitte lade die Seite manuell neu (ggf. mit Strg+F5), oder prüfe die Verbindung zum Server.'
|
||||
: 'Beim Initialisieren der Oberfläche ist ein Fehler aufgetreten.'}
|
||||
</div>
|
||||
<pre style={{
|
||||
|
||||
Reference in New Issue
Block a user