21a315ca71
Stefan ist unterwegs, ADB-Zugriff nicht moeglich. Loesung: die App
loggt ihre eigenen Crashes via RVS, Bridge sammelt sie in
/shared/logs/app.log, Diagnostic-Server liefert sie als JSON.
Damit braucht's keinen ADB mehr — Crashes sind sofort vom Browser
(oder Claude per curl) lesbar.
Komponenten:
1. App components/ErrorBoundary.tsx
- React-ErrorBoundary fuer kritische Sections
- componentDidCatch → reportAppError (RVS-Send)
- UI zeigt Error-Box statt White-Screen + Reset-Button
2. App services/logger.ts
- reportAppError(scope, message, stack) → rvs.send('app_log', ...)
- installGlobalCrashReporter() haengt sich an ErrorUtils.setGlobalHandler
UND HermesInternal.enablePromiseRejectionTracker — fangt sowohl
ungefangene Errors als auch unhandled Promise-Rejections
- Konsole bleibt parallel aktiv (damit ADB im Dev-Build weiter
was sieht)
3. App App.tsx: installGlobalCrashReporter() im useEffect zusammen
mit initLogger.
4. App ChatScreen.tsx:
- Inbox-Modal mit ErrorBoundary umschlossen (scope: InboxModal,
onReset schliesst Modal)
- MemoryDetailModal mit ErrorBoundary umschlossen
- DetailModal wird nur noch konditional gerendert (memoryDetailId
!= null) statt immer visible-toggle — vermeidet potentielles
Modal-Stacking-Problem
5. RVS server.js: ALLOWED_TYPES += "app_log"
6. Bridge aria_bridge.py:
- elif msg_type == "app_log": haengt eine Zeile an
/shared/logs/app.log (JSONL, jedes Item {ts, platform, level,
scope, message, stack})
- Plus log.info Hinweis fuer das normale Bridge-Log
7. Diagnostic server.js:
- GET /api/app-log[?limit=N] → letzte N Eintraege als JSON
- POST /api/app-log/clear → log-Datei loeschen
Workflow zum Debuggen des Inbox-Crashes:
Stefan rebuilded App → drueckt Inbox → ErrorBoundary fangt den
Crash (oder Global-Handler bei ungefangenem Error) → reportAppError
→ RVS → Bridge schreibt nach /shared/logs/app.log → Stefan
oder Claude rufen GET /api/app-log auf → sehen Stacktrace.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
90 lines
3.1 KiB
TypeScript
90 lines
3.1 KiB
TypeScript
/**
|
||
* ErrorBoundary — fängt React-Render-Fehler und zeigt eine Error-Box
|
||
* statt White-Screen-of-Death. Plus: Crash wird zum logger geschickt,
|
||
* der das ueber RVS an die Bridge weiterleitet.
|
||
*
|
||
* Einsatz: kritische Komponenten/Modals damit ein Bug nicht die ganze
|
||
* App killt.
|
||
*/
|
||
|
||
import React from 'react';
|
||
import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||
|
||
import { reportAppError } from '../services/logger';
|
||
|
||
interface Props {
|
||
children: React.ReactNode;
|
||
/** Optional: Bezeichnung der eingegrenzten Section fuer's Log. */
|
||
scope?: string;
|
||
/** Optional: Reset-Callback (z.B. Modal schliessen) — Button ist dann sichtbar. */
|
||
onReset?: () => void;
|
||
}
|
||
|
||
interface State {
|
||
err: Error | null;
|
||
info: string;
|
||
}
|
||
|
||
export class ErrorBoundary extends React.Component<Props, State> {
|
||
constructor(props: Props) {
|
||
super(props);
|
||
this.state = { err: null, info: '' };
|
||
}
|
||
|
||
static getDerivedStateFromError(err: Error): Partial<State> {
|
||
return { err };
|
||
}
|
||
|
||
componentDidCatch(err: Error, info: any) {
|
||
const stack = info?.componentStack || '';
|
||
this.setState({ info: stack });
|
||
reportAppError({
|
||
scope: this.props.scope || 'ErrorBoundary',
|
||
message: err?.message || String(err),
|
||
stack: (err?.stack || '') + '\n--- componentStack ---\n' + stack,
|
||
});
|
||
}
|
||
|
||
render() {
|
||
if (this.state.err) {
|
||
return (
|
||
<View style={s.box}>
|
||
<Text style={s.title}>⚠️ Etwas ist schiefgegangen</Text>
|
||
<Text style={s.scope}>{this.props.scope || 'unbekannte Komponente'}</Text>
|
||
<ScrollView style={s.scroll}>
|
||
<Text style={s.msg}>{this.state.err.message || String(this.state.err)}</Text>
|
||
{this.state.info ? <Text style={s.stack}>{this.state.info}</Text> : null}
|
||
</ScrollView>
|
||
{this.props.onReset ? (
|
||
<TouchableOpacity style={s.btn} onPress={() => { this.setState({err:null,info:''}); this.props.onReset?.(); }}>
|
||
<Text style={s.btnText}>Schliessen + zurueck</Text>
|
||
</TouchableOpacity>
|
||
) : (
|
||
<TouchableOpacity style={s.btn} onPress={() => this.setState({err:null,info:''})}>
|
||
<Text style={s.btnText}>Erneut versuchen</Text>
|
||
</TouchableOpacity>
|
||
)}
|
||
<Text style={s.hint}>
|
||
Crash wurde an die Bridge gemeldet — sichtbar in der Diagnostic-Web-UI unter /api/app-log
|
||
</Text>
|
||
</View>
|
||
);
|
||
}
|
||
return this.props.children;
|
||
}
|
||
}
|
||
|
||
const s = StyleSheet.create({
|
||
box: { flex:1, padding:16, backgroundColor:'#1A0A0A' },
|
||
title: { color:'#FF6B6B', fontWeight:'bold', fontSize:16, marginBottom:6 },
|
||
scope: { color:'#FF9500', fontSize:12, marginBottom:10 },
|
||
scroll: { flex:1, backgroundColor:'#0D0D1A', borderRadius:6, padding:10, marginBottom:10 },
|
||
msg: { color:'#FF6B6B', fontSize:13, marginBottom:8 },
|
||
stack: { color:'#8888AA', fontSize:11, fontFamily:'monospace' },
|
||
btn: { backgroundColor:'#0096FF', paddingVertical:10, borderRadius:6, alignItems:'center' },
|
||
btnText: { color:'#fff', fontWeight:'600' },
|
||
hint: { color:'#555570', fontSize:10, marginTop:8, textAlign:'center' },
|
||
});
|
||
|
||
export default ErrorBoundary;
|