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:
Debian
2026-06-04 12:10:41 +02:00
parent 025854150d
commit f7dd7a3a4b
11 changed files with 582 additions and 19 deletions

View File

@@ -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={{