Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f11f28448 | |||
| 21a315ca71 | |||
| d8b05082d6 | |||
| de91073b2e | |||
| e88b5f57bf | |||
| 64a17c8c19 | |||
| ebeacba8b5 |
+4
-1
@@ -13,7 +13,7 @@ import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||
import ChatScreen from './src/screens/ChatScreen';
|
||||
import SettingsScreen from './src/screens/SettingsScreen';
|
||||
import rvs from './src/services/rvs';
|
||||
import { initLogger } from './src/services/logger';
|
||||
import { initLogger, installGlobalCrashReporter } from './src/services/logger';
|
||||
|
||||
// --- Navigation ---
|
||||
|
||||
@@ -49,6 +49,9 @@ const App: React.FC = () => {
|
||||
// initLogger ist async aber blockt nichts — solange er noch laueft,
|
||||
// loggen wir normal (Default an), danach respektiert console.log das Setting.
|
||||
initLogger().catch(() => {});
|
||||
// Crash-Reporter installieren — ungefangene JS-Errors landen via RVS
|
||||
// bei der Bridge (sichtbar in /shared/logs/app.log + Diagnostic-API)
|
||||
installGlobalCrashReporter();
|
||||
const initConnection = async () => {
|
||||
const config = await rvs.loadConfig();
|
||||
if (config) {
|
||||
|
||||
@@ -79,8 +79,8 @@ android {
|
||||
applicationId "com.ariacockpit"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 10301
|
||||
versionName "0.1.3.1"
|
||||
versionCode 10305
|
||||
versionName "0.1.3.5"
|
||||
// Fallback fuer Libraries mit Product Flavors
|
||||
missingDimensionStrategy 'react-native-camera', 'general'
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "aria-cockpit",
|
||||
"version": "0.1.3.1",
|
||||
"version": "0.1.3.5",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* 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;
|
||||
@@ -37,9 +37,11 @@ interface Props {
|
||||
title?: string;
|
||||
/** Style-Erweiterung fuer den Container. */
|
||||
flatStyle?: boolean;
|
||||
/** Wenn gesetzt: kein eigenes DetailModal mounten — Parent kuemmert sich. */
|
||||
onOpenMemory?: (id: string) => void;
|
||||
}
|
||||
|
||||
export const MemoryBrowser: React.FC<Props> = ({ restrictToIds, title, flatStyle }) => {
|
||||
export const MemoryBrowser: React.FC<Props> = ({ restrictToIds, title, flatStyle, onOpenMemory }) => {
|
||||
const [items, setItems] = useState<Memory[]>([]);
|
||||
const [filtered, setFiltered] = useState<Memory[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -82,38 +84,35 @@ export const MemoryBrowser: React.FC<Props> = ({ restrictToIds, title, flatStyle
|
||||
setFiltered(out);
|
||||
}, [items, q, typeFilter, pinnedFilter, restrictToIds]);
|
||||
|
||||
const [showNewMemoryDialog, setShowNewMemoryDialog] = useState(false);
|
||||
const [newMemoryTitle, setNewMemoryTitle] = useState('');
|
||||
|
||||
const onAddNew = () => {
|
||||
Alert.prompt(
|
||||
'Neue Memory',
|
||||
'Titel:',
|
||||
[
|
||||
{ text: 'Abbrechen', style: 'cancel' },
|
||||
{
|
||||
text: 'Anlegen',
|
||||
onPress: async (title?: string) => {
|
||||
const t = (title || '').trim();
|
||||
if (!t) return;
|
||||
try {
|
||||
const m = await brainApi.saveMemory({
|
||||
type: 'fact', title: t,
|
||||
content: '(noch leer — bitte editieren)',
|
||||
});
|
||||
load();
|
||||
setOpenId(m.id);
|
||||
} catch (e: any) {
|
||||
Alert.alert('Fehler', String(e?.message || e));
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
'plain-text',
|
||||
);
|
||||
setNewMemoryTitle('');
|
||||
setShowNewMemoryDialog(true);
|
||||
};
|
||||
|
||||
const confirmAddNew = async () => {
|
||||
const t = newMemoryTitle.trim();
|
||||
if (!t) { setShowNewMemoryDialog(false); return; }
|
||||
setShowNewMemoryDialog(false);
|
||||
try {
|
||||
const m = await brainApi.saveMemory({
|
||||
type: 'fact', title: t,
|
||||
content: '(noch leer — bitte editieren)',
|
||||
});
|
||||
load();
|
||||
if (onOpenMemory) onOpenMemory(m.id);
|
||||
else setOpenId(m.id);
|
||||
} catch (e: any) {
|
||||
Alert.alert('Fehler', String(e?.message || e));
|
||||
}
|
||||
};
|
||||
|
||||
const renderItem = ({ item }: { item: Memory }) => {
|
||||
const attCount = (item.attachments || []).length;
|
||||
return (
|
||||
<TouchableOpacity style={s.row} onPress={() => setOpenId(item.id)}>
|
||||
<TouchableOpacity style={s.row} onPress={() => onOpenMemory ? onOpenMemory(item.id) : setOpenId(item.id)}>
|
||||
<View style={{flex:1}}>
|
||||
<Text style={s.rowTitle} numberOfLines={1}>
|
||||
{item.pinned ? '📌 ' : ''}{item.title || '(ohne Titel)'}
|
||||
@@ -202,12 +201,42 @@ export const MemoryBrowser: React.FC<Props> = ({ restrictToIds, title, flatStyle
|
||||
</TouchableOpacity>
|
||||
</Modal>
|
||||
|
||||
<MemoryDetailModal
|
||||
memoryId={openId}
|
||||
visible={!!openId}
|
||||
onClose={() => { setOpenId(null); load(); }}
|
||||
onDeleted={() => { setOpenId(null); load(); }}
|
||||
/>
|
||||
{/* Eigenes DetailModal nur wenn der Parent kein Callback uebergibt
|
||||
(vermeidet Modal-in-Modal-Stacking auf Android). */}
|
||||
{!onOpenMemory && (
|
||||
<MemoryDetailModal
|
||||
memoryId={openId}
|
||||
visible={!!openId}
|
||||
onClose={() => { setOpenId(null); load(); }}
|
||||
onDeleted={() => { setOpenId(null); load(); }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* "Neue Memory"-Dialog (Alert.prompt ist iOS-only, daher eigenes Modal) */}
|
||||
<Modal visible={showNewMemoryDialog} transparent animationType="fade" onRequestClose={() => setShowNewMemoryDialog(false)}>
|
||||
<View style={s.menuBack}>
|
||||
<View style={[s.menuBox, {padding:16, minWidth:280}]}>
|
||||
<Text style={{color:'#FFD60A', fontWeight:'bold', fontSize:14, marginBottom:10}}>Neue Memory anlegen</Text>
|
||||
<Text style={{color:'#8888AA', fontSize:11, marginBottom:6}}>Titel:</Text>
|
||||
<TextInput
|
||||
value={newMemoryTitle}
|
||||
onChangeText={setNewMemoryTitle}
|
||||
autoFocus
|
||||
placeholder="z.B. Stefans Auto"
|
||||
placeholderTextColor="#555570"
|
||||
style={{backgroundColor:'#1E1E2E', color:'#E0E0F0', padding:8, borderRadius:4, fontSize:13, marginBottom:12}}
|
||||
/>
|
||||
<View style={{flexDirection:'row', gap:8, justifyContent:'flex-end'}}>
|
||||
<TouchableOpacity onPress={() => setShowNewMemoryDialog(false)} style={{padding:8}}>
|
||||
<Text style={{color:'#8888AA'}}>Abbrechen</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={confirmAddNew} style={{backgroundColor:'#0096FF', paddingHorizontal:14, paddingVertical:8, borderRadius:4}}>
|
||||
<Text style={{color:'#fff', fontWeight:'600'}}>Anlegen</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -30,6 +30,7 @@ import { Dimensions } from 'react-native';
|
||||
import ZoomableImage from '../components/ZoomableImage';
|
||||
import MemoryDetailModal from '../components/MemoryDetailModal';
|
||||
import MemoryBrowser from '../components/MemoryBrowser';
|
||||
import ErrorBoundary from '../components/ErrorBoundary';
|
||||
import rvs, { RVSMessage, ConnectionState } from '../services/rvs';
|
||||
import audioService from '../services/audio';
|
||||
import wakeWordService from '../services/wakeword';
|
||||
@@ -1026,7 +1027,15 @@ const ChatScreen: React.FC = () => {
|
||||
}, [messages]);
|
||||
|
||||
// Inverted FlatList: neueste Nachrichten unten, kein manuelles Scrollen noetig
|
||||
const invertedMessages = useMemo(() => [...messages].reverse(), [messages]);
|
||||
// Spezial-Bubbles (memorySaved/triggerCreated/skillCreated) sollen im Chat
|
||||
// NICHT mehr erscheinen — sie werden in der Notizen-Inbox angezeigt.
|
||||
// Das verhindert dass sie chronologisch unten im Chat haengen und der
|
||||
// eigentliche Chat-Verlauf darunter verschwindet.
|
||||
const chatVisibleMessages = useMemo(
|
||||
() => messages.filter(m => !m.memorySaved && !m.triggerCreated && !m.skillCreated),
|
||||
[messages],
|
||||
);
|
||||
const invertedMessages = useMemo(() => [...chatVisibleMessages].reverse(), [chatVisibleMessages]);
|
||||
|
||||
// Such-Treffer: alle Message-IDs die zur Query passen, in chronologischer
|
||||
// Reihenfolge (aelteste zuerst). Bei Query-Change resetten wir den Index.
|
||||
@@ -1597,7 +1606,7 @@ const ChatScreen: React.FC = () => {
|
||||
connectionState === 'connecting' ? 'Verbinde...' : 'Getrennt'}
|
||||
</Text>
|
||||
<TouchableOpacity onPress={() => setInboxVisible(true)} style={{marginLeft: 'auto', paddingHorizontal: 6}} hitSlop={{top:8,bottom:8,left:6,right:6}}>
|
||||
<Text style={{fontSize: 18}}>\uD83D\uDDC2\uFE0F</Text>
|
||||
<Text style={{fontSize: 18}}>{'\uD83D\uDDC2\uFE0F'}</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={() => setSearchVisible(!searchVisible)} style={{paddingHorizontal: 6}} hitSlop={{top:8,bottom:8,left:6,right:6}}>
|
||||
<Text style={{fontSize: 16}}>{'\uD83D\uDD0D'}</Text>
|
||||
@@ -1831,26 +1840,108 @@ const ChatScreen: React.FC = () => {
|
||||
</View>
|
||||
|
||||
{/* Memory-Detail/Edit-Modal — wird durch Tap auf eine memorySaved-Bubble geoeffnet */}
|
||||
<MemoryDetailModal
|
||||
memoryId={memoryDetailId}
|
||||
visible={!!memoryDetailId}
|
||||
onClose={() => setMemoryDetailId(null)}
|
||||
onDeleted={() => setMemoryDetailId(null)}
|
||||
/>
|
||||
{memoryDetailId ? (
|
||||
<ErrorBoundary scope="ChatScreen.MemoryDetailModal" onReset={() => setMemoryDetailId(null)}>
|
||||
<MemoryDetailModal
|
||||
memoryId={memoryDetailId}
|
||||
visible={!!memoryDetailId}
|
||||
onClose={() => setMemoryDetailId(null)}
|
||||
onDeleted={() => setMemoryDetailId(null)}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
) : null}
|
||||
|
||||
{/* Notizen-Inbox — Listet alle Memories aus dem aktuellen Chat (Special-Bubbles).
|
||||
Bestes-Aus-beiden-Welten: nur die Memory-IDs aus den memorySaved-Bubbles
|
||||
des aktuellen Chats, plus den vollen Browser darunter wenn der User mehr will. */}
|
||||
<Modal visible={inboxVisible} animationType="slide" onRequestClose={() => setInboxVisible(false)}>
|
||||
<ErrorBoundary scope="ChatScreen.InboxModal" onReset={() => setInboxVisible(false)}>
|
||||
<View style={{flex:1, backgroundColor:'#0D0D1A'}}>
|
||||
<View style={{flexDirection:'row', alignItems:'center', padding:14, borderBottomWidth:1, borderBottomColor:'#1E1E2E'}}>
|
||||
<Text style={{color:'#FFD60A', fontWeight:'bold', fontSize:16, flex:1}}>🗂️ Notizen-Inbox</Text>
|
||||
<Text style={{color:'#FFD60A', fontWeight:'bold', fontSize:16, flex:1}}>{'🗂️'} Notizen-Inbox</Text>
|
||||
<TouchableOpacity onPress={() => setInboxVisible(false)} hitSlop={{top:8,bottom:8,left:8,right:8}}>
|
||||
<Text style={{color:'#8888AA', fontSize:24}}>×</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<MemoryBrowser />
|
||||
{/* Aus aktuellem Chat: Spezial-Bubbles (memory/trigger/skill) kompakt
|
||||
auflisten — neueste oben. Klick auf Memory oeffnet Detail-Modal. */}
|
||||
{(() => {
|
||||
const specials = messages
|
||||
.filter(m => m.memorySaved || m.triggerCreated || m.skillCreated)
|
||||
.slice().reverse();
|
||||
if (specials.length === 0) {
|
||||
return (
|
||||
<View style={{padding:14, borderBottomWidth:1, borderBottomColor:'#1E1E2E'}}>
|
||||
<Text style={{color:'#555570', fontSize:11, fontStyle:'italic'}}>
|
||||
(keine Notizen-Bubbles im aktuellen Chat)
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<View style={{maxHeight:260, borderBottomWidth:1, borderBottomColor:'#1E1E2E'}}>
|
||||
<Text style={{color:'#8888AA', fontSize:11, paddingHorizontal:14, paddingTop:8, paddingBottom:4, textTransform:'uppercase', letterSpacing:0.5}}>
|
||||
Aus diesem Chat
|
||||
</Text>
|
||||
<ScrollView style={{paddingHorizontal:8}}>
|
||||
{specials.map(m => {
|
||||
if (m.memorySaved) {
|
||||
const ms = m.memorySaved;
|
||||
const action = ms.action || 'created';
|
||||
const verb = action === 'updated' ? 'geändert' : action === 'deleted' ? 'gelöscht' : 'angelegt';
|
||||
const dotColor = action === 'deleted' ? '#FF6B6B' : '#FFD60A';
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={m.id}
|
||||
style={styles.inboxRow}
|
||||
onPress={() => { if (ms.id && action !== 'deleted') { setInboxVisible(false); setMemoryDetailId(ms.id); } }}
|
||||
disabled={!ms.id || action === 'deleted'}
|
||||
>
|
||||
<Text style={{fontSize:16}}>{'🧠'}</Text>
|
||||
<View style={{flex:1}}>
|
||||
<Text style={styles.inboxRowTitle} numberOfLines={1}>{ms.title}</Text>
|
||||
<Text style={[styles.inboxRowMeta, {color: dotColor}]}>Memory · {verb} · {ms.type}</Text>
|
||||
</View>
|
||||
{ms.id && action !== 'deleted' ? <Text style={{color:'#0096FF', fontSize:14}}>›</Text> : null}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
if (m.triggerCreated) {
|
||||
const t = m.triggerCreated;
|
||||
return (
|
||||
<View key={m.id} style={styles.inboxRow}>
|
||||
<Text style={{fontSize:16}}>{'⏰'}</Text>
|
||||
<View style={{flex:1}}>
|
||||
<Text style={styles.inboxRowTitle} numberOfLines={1}>{t.name}</Text>
|
||||
<Text style={styles.inboxRowMeta}>Trigger · {t.type}{t.fires_at ? ` · ${t.fires_at.slice(0,16).replace('T',' ')}` : ''}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
if (m.skillCreated) {
|
||||
const sk = m.skillCreated;
|
||||
return (
|
||||
<View key={m.id} style={styles.inboxRow}>
|
||||
<Text style={{fontSize:16}}>{'🛠'}</Text>
|
||||
<View style={{flex:1}}>
|
||||
<Text style={styles.inboxRowTitle} numberOfLines={1}>{sk.name}</Text>
|
||||
<Text style={styles.inboxRowMeta}>Skill · {sk.execution}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
})()}
|
||||
<Text style={{color:'#8888AA', fontSize:11, paddingHorizontal:14, paddingTop:10, paddingBottom:4, textTransform:'uppercase', letterSpacing:0.5}}>
|
||||
Alle Memories aus der DB
|
||||
</Text>
|
||||
<MemoryBrowser onOpenMemory={(id) => { setInboxVisible(false); setMemoryDetailId(id); }} />
|
||||
</View>
|
||||
</ErrorBoundary>
|
||||
</Modal>
|
||||
|
||||
{/* Bild-Vollbild Modal */}
|
||||
@@ -2181,6 +2272,25 @@ const styles = StyleSheet.create({
|
||||
playButtonText: {
|
||||
fontSize: 16,
|
||||
},
|
||||
inboxRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
backgroundColor: '#1E1E2E',
|
||||
padding: 10,
|
||||
borderRadius: 6,
|
||||
marginBottom: 4,
|
||||
},
|
||||
inboxRowTitle: {
|
||||
color: '#E0E0F0',
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
},
|
||||
inboxRowMeta: {
|
||||
color: '#8888AA',
|
||||
fontSize: 11,
|
||||
marginTop: 1,
|
||||
},
|
||||
memoryAttachmentRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
*/
|
||||
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { Platform } from 'react-native';
|
||||
import rvs from './rvs';
|
||||
|
||||
export const VERBOSE_LOGGING_KEY = 'aria_verbose_logging';
|
||||
|
||||
@@ -39,3 +41,77 @@ export function setVerboseLogging(verbose: boolean): void {
|
||||
applyState();
|
||||
AsyncStorage.setItem(VERBOSE_LOGGING_KEY, String(verbose)).catch(() => {});
|
||||
}
|
||||
|
||||
// ─── App-Crash-Reporting via RVS ────────────────────────────────────
|
||||
//
|
||||
// Wenn die App crasht — egal ob React-Render-Fehler (ErrorBoundary) oder
|
||||
// ungefangener JS-Error (ErrorUtils-Handler) — schicken wir den Crash
|
||||
// als RVS-Message vom Typ "app_log" an die Bridge. Die schreibt in
|
||||
// /shared/logs/app.log, sodass wir/Diagnostic die Crashes mitlesen
|
||||
// koennen ohne ADB.
|
||||
|
||||
interface AppErrorEvent {
|
||||
scope: string;
|
||||
message: string;
|
||||
stack?: string;
|
||||
level?: 'error' | 'warn' | 'info';
|
||||
}
|
||||
|
||||
let _reportingInstalled = false;
|
||||
|
||||
/** Schickt einen App-Fehler via RVS an die Bridge. */
|
||||
export function reportAppError(ev: AppErrorEvent): void {
|
||||
try {
|
||||
rvs.send('app_log' as any, {
|
||||
ts: Date.now(),
|
||||
platform: Platform.OS,
|
||||
level: ev.level || 'error',
|
||||
scope: ev.scope,
|
||||
message: ev.message,
|
||||
stack: (ev.stack || '').slice(0, 8000),
|
||||
});
|
||||
} catch {
|
||||
// RVS noch nicht connected — Fehler geht im console weiter.
|
||||
}
|
||||
// Plus lokal: console.error, damit Stefan's adb (wenn doch mal verfuegbar)
|
||||
// den Crash sieht.
|
||||
console.error(`[app-error scope=${ev.scope}]`, ev.message, '\n', ev.stack || '');
|
||||
}
|
||||
|
||||
/** Installiert einen globalen JS-Error-Handler der ungefangene Errors via
|
||||
* RVS an die Bridge schickt. Beim App-Start aufrufen. */
|
||||
export function installGlobalCrashReporter(): void {
|
||||
if (_reportingInstalled) return;
|
||||
_reportingInstalled = true;
|
||||
try {
|
||||
const g: any = global as any;
|
||||
const prev = g.ErrorUtils?.getGlobalHandler?.();
|
||||
g.ErrorUtils?.setGlobalHandler?.((err: any, isFatal: boolean) => {
|
||||
reportAppError({
|
||||
scope: isFatal ? 'global-fatal' : 'global-nonfatal',
|
||||
message: (err && err.message) || String(err),
|
||||
stack: err && err.stack,
|
||||
});
|
||||
// Original-Handler weiterhin aufrufen damit React-Native das System-
|
||||
// Crash-Overlay zeigt (im Dev-Build) bzw. in Production sauber stirbt.
|
||||
if (typeof prev === 'function') {
|
||||
try { prev(err, isFatal); } catch {}
|
||||
}
|
||||
});
|
||||
// unhandled Promise-Rejections — manche RN-Versionen haben das nicht
|
||||
// automatisch im ErrorUtils.
|
||||
g.HermesInternal?.enablePromiseRejectionTracker?.({
|
||||
allRejections: true,
|
||||
onUnhandled: (id: number, err: any) => {
|
||||
reportAppError({
|
||||
scope: 'promise-unhandled',
|
||||
level: 'warn',
|
||||
message: (err && err.message) || String(err),
|
||||
stack: err && err.stack,
|
||||
});
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
// ErrorUtils nicht da → nix machen
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1824,6 +1824,29 @@ class ARIABridge:
|
||||
logger.warning("[rvs] delete_message fehlgeschlagen: %s", result.get("error"))
|
||||
return
|
||||
|
||||
elif msg_type == "app_log":
|
||||
# App schickt Crash/Error/Info-Log via RVS — wir schreiben das
|
||||
# in /shared/logs/app.log (JSONL) damit Diagnostic + Claude
|
||||
# mitlesen koennen, auch ohne ADB-Zugriff aufs Handy.
|
||||
try:
|
||||
log_dir = Path("/shared/logs")
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
line = {
|
||||
"ts": payload.get("ts") or int(time.time() * 1000),
|
||||
"platform": payload.get("platform", "?"),
|
||||
"level": payload.get("level", "info"),
|
||||
"scope": payload.get("scope", ""),
|
||||
"message": payload.get("message", ""),
|
||||
"stack": payload.get("stack", ""),
|
||||
}
|
||||
with (log_dir / "app.log").open("a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(line, ensure_ascii=False) + "\n")
|
||||
logger.info("[app-log] %s %s: %s",
|
||||
line["level"], line["scope"], line["message"][:120])
|
||||
except Exception as exc:
|
||||
logger.warning("[app-log] schreiben fehlgeschlagen: %s", exc)
|
||||
return
|
||||
|
||||
elif msg_type == "brain_request":
|
||||
# Generischer RVS-Proxy fuer die Brain-HTTP-API.
|
||||
# payload: {requestId, method, path, body?, bodyBase64?, contentType?}
|
||||
|
||||
@@ -1338,6 +1338,42 @@ const server = http.createServer((req, res) => {
|
||||
else broadcast({ type: "agent_activity", activity: "idle" });
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ ok: true }));
|
||||
} else if (req.url.startsWith("/api/app-log") && req.method === "GET") {
|
||||
// App-Crash-Reporting-Log lesen — die App schickt JS-Errors via RVS,
|
||||
// Bridge schreibt JSONL nach /shared/logs/app.log. Wir liefern die
|
||||
// letzten 200 Eintraege (oder ?limit=N).
|
||||
const url = new URL(req.url, "http://x");
|
||||
const limit = Math.max(1, Math.min(2000, parseInt(url.searchParams.get("limit") || "200", 10) || 200));
|
||||
try {
|
||||
const file = "/shared/logs/app.log";
|
||||
if (!fs.existsSync(file)) {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ count: 0, entries: [] }));
|
||||
return;
|
||||
}
|
||||
const raw = fs.readFileSync(file, "utf-8");
|
||||
const lines = raw.split("\n").filter(l => l.trim());
|
||||
const tail = lines.slice(-limit);
|
||||
const entries = tail.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ count: entries.length, entries }));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ error: err.message }));
|
||||
}
|
||||
return;
|
||||
} else if (req.url === "/api/app-log/clear" && req.method === "POST") {
|
||||
// App-Log leeren — nach erfolgreichem Debug.
|
||||
try {
|
||||
const file = "/shared/logs/app.log";
|
||||
if (fs.existsSync(file)) fs.unlinkSync(file);
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ ok: true }));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ ok: false, error: err.message }));
|
||||
}
|
||||
return;
|
||||
} else if (req.url === "/api/files-list" && req.method === "GET") {
|
||||
// Liste alle Dateien in /shared/uploads/ — die kommen entweder vom User
|
||||
// (Upload aus App/Diagnostic) oder von ARIA (aria_<name>.<ext> Pattern).
|
||||
|
||||
@@ -31,6 +31,7 @@ const ALLOWED_TYPES = new Set([
|
||||
"chat_history_request", "chat_history_response", "chat_cleared",
|
||||
"delete_message_request", "chat_message_deleted",
|
||||
"brain_request", "brain_response",
|
||||
"app_log",
|
||||
"file_delete_batch_request", "file_delete_batch_response",
|
||||
"file_zip_request", "file_zip_response",
|
||||
"xtts_delete_voice",
|
||||
|
||||
Reference in New Issue
Block a user