Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b28a065c0 | |||
| e74e1eaf70 | |||
| ff7c6333bb | |||
| 2c85df3499 | |||
| 6f11f28448 | |||
| 21a315ca71 | |||
| d8b05082d6 |
@@ -25,6 +25,10 @@ aria-data/brain-import/*
|
|||||||
!aria-data/brain-import/.gitkeep
|
!aria-data/brain-import/.gitkeep
|
||||||
!aria-data/brain-import/README.md
|
!aria-data/brain-import/README.md
|
||||||
|
|
||||||
|
# .aria-debug/ — App-Crash-Logs die tools/fetch-app-logs.sh hier ablegt.
|
||||||
|
# Komplett lokal, enthaelt potentiell private Stacktraces / Daten.
|
||||||
|
.aria-debug/
|
||||||
|
|
||||||
# ── ARIAs Gedächtnis (Vector-DB, Skills, Models) ──
|
# ── ARIAs Gedächtnis (Vector-DB, Skills, Models) ──
|
||||||
# Backup via Diagnostic → Gehirn-Export (tar.gz), nicht via Git.
|
# Backup via Diagnostic → Gehirn-Export (tar.gz), nicht via Git.
|
||||||
aria-data/brain/data/
|
aria-data/brain/data/
|
||||||
|
|||||||
+4
-1
@@ -13,7 +13,7 @@ import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
|||||||
import ChatScreen from './src/screens/ChatScreen';
|
import ChatScreen from './src/screens/ChatScreen';
|
||||||
import SettingsScreen from './src/screens/SettingsScreen';
|
import SettingsScreen from './src/screens/SettingsScreen';
|
||||||
import rvs from './src/services/rvs';
|
import rvs from './src/services/rvs';
|
||||||
import { initLogger } from './src/services/logger';
|
import { initLogger, installGlobalCrashReporter } from './src/services/logger';
|
||||||
|
|
||||||
// --- Navigation ---
|
// --- Navigation ---
|
||||||
|
|
||||||
@@ -49,6 +49,9 @@ const App: React.FC = () => {
|
|||||||
// initLogger ist async aber blockt nichts — solange er noch laueft,
|
// initLogger ist async aber blockt nichts — solange er noch laueft,
|
||||||
// loggen wir normal (Default an), danach respektiert console.log das Setting.
|
// loggen wir normal (Default an), danach respektiert console.log das Setting.
|
||||||
initLogger().catch(() => {});
|
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 initConnection = async () => {
|
||||||
const config = await rvs.loadConfig();
|
const config = await rvs.loadConfig();
|
||||||
if (config) {
|
if (config) {
|
||||||
|
|||||||
@@ -79,8 +79,8 @@ android {
|
|||||||
applicationId "com.ariacockpit"
|
applicationId "com.ariacockpit"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 10303
|
versionCode 10307
|
||||||
versionName "0.1.3.3"
|
versionName "0.1.3.7"
|
||||||
// Fallback fuer Libraries mit Product Flavors
|
// Fallback fuer Libraries mit Product Flavors
|
||||||
missingDimensionStrategy 'react-native-camera', 'general'
|
missingDimensionStrategy 'react-native-camera', 'general'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "aria-cockpit",
|
"name": "aria-cockpit",
|
||||||
"version": "0.1.3.3",
|
"version": "0.1.3.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"android": "react-native run-android",
|
"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;
|
||||||
@@ -30,6 +30,7 @@ import { Dimensions } from 'react-native';
|
|||||||
import ZoomableImage from '../components/ZoomableImage';
|
import ZoomableImage from '../components/ZoomableImage';
|
||||||
import MemoryDetailModal from '../components/MemoryDetailModal';
|
import MemoryDetailModal from '../components/MemoryDetailModal';
|
||||||
import MemoryBrowser from '../components/MemoryBrowser';
|
import MemoryBrowser from '../components/MemoryBrowser';
|
||||||
|
import ErrorBoundary from '../components/ErrorBoundary';
|
||||||
import rvs, { RVSMessage, ConnectionState } from '../services/rvs';
|
import rvs, { RVSMessage, ConnectionState } from '../services/rvs';
|
||||||
import audioService from '../services/audio';
|
import audioService from '../services/audio';
|
||||||
import wakeWordService from '../services/wakeword';
|
import wakeWordService from '../services/wakeword';
|
||||||
@@ -1839,17 +1840,22 @@ const ChatScreen: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Memory-Detail/Edit-Modal — wird durch Tap auf eine memorySaved-Bubble geoeffnet */}
|
{/* Memory-Detail/Edit-Modal — wird durch Tap auf eine memorySaved-Bubble geoeffnet */}
|
||||||
<MemoryDetailModal
|
{memoryDetailId ? (
|
||||||
memoryId={memoryDetailId}
|
<ErrorBoundary scope="ChatScreen.MemoryDetailModal" onReset={() => setMemoryDetailId(null)}>
|
||||||
visible={!!memoryDetailId}
|
<MemoryDetailModal
|
||||||
onClose={() => setMemoryDetailId(null)}
|
memoryId={memoryDetailId}
|
||||||
onDeleted={() => setMemoryDetailId(null)}
|
visible={!!memoryDetailId}
|
||||||
/>
|
onClose={() => setMemoryDetailId(null)}
|
||||||
|
onDeleted={() => setMemoryDetailId(null)}
|
||||||
|
/>
|
||||||
|
</ErrorBoundary>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{/* Notizen-Inbox — Listet alle Memories aus dem aktuellen Chat (Special-Bubbles).
|
{/* Notizen-Inbox — Listet alle Memories aus dem aktuellen Chat (Special-Bubbles).
|
||||||
Bestes-Aus-beiden-Welten: nur die Memory-IDs aus den memorySaved-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. */}
|
des aktuellen Chats, plus den vollen Browser darunter wenn der User mehr will. */}
|
||||||
<Modal visible={inboxVisible} animationType="slide" onRequestClose={() => setInboxVisible(false)}>
|
<Modal visible={inboxVisible} animationType="slide" onRequestClose={() => setInboxVisible(false)}>
|
||||||
|
<ErrorBoundary scope="ChatScreen.InboxModal" onReset={() => setInboxVisible(false)}>
|
||||||
<View style={{flex:1, backgroundColor:'#0D0D1A'}}>
|
<View style={{flex:1, backgroundColor:'#0D0D1A'}}>
|
||||||
<View style={{flexDirection:'row', alignItems:'center', padding:14, borderBottomWidth:1, borderBottomColor:'#1E1E2E'}}>
|
<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>
|
||||||
@@ -1935,6 +1941,7 @@ const ChatScreen: React.FC = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
<MemoryBrowser onOpenMemory={(id) => { setInboxVisible(false); setMemoryDetailId(id); }} />
|
<MemoryBrowser onOpenMemory={(id) => { setInboxVisible(false); setMemoryDetailId(id); }} />
|
||||||
</View>
|
</View>
|
||||||
|
</ErrorBoundary>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{/* Bild-Vollbild Modal */}
|
{/* Bild-Vollbild Modal */}
|
||||||
|
|||||||
@@ -54,6 +54,18 @@ function _newRequestId(): string {
|
|||||||
return `brain_${Date.now().toString(36)}_${_nextId}`;
|
return `brain_${Date.now().toString(36)}_${_nextId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Mini-Query-String-Builder ohne URLSearchParams (Hermes-Polyfill kennt
|
||||||
|
* kein URLSearchParams.set, crasht). Akzeptiert object mit string/number/
|
||||||
|
* bool-Values; undefined/null/leere Strings werden ausgelassen. */
|
||||||
|
function _qs(params: Record<string, unknown>): string {
|
||||||
|
const parts: string[] = [];
|
||||||
|
for (const [k, v] of Object.entries(params)) {
|
||||||
|
if (v === undefined || v === null || v === '') continue;
|
||||||
|
parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`);
|
||||||
|
}
|
||||||
|
return parts.length ? `?${parts.join('&')}` : '';
|
||||||
|
}
|
||||||
|
|
||||||
interface SendOpts {
|
interface SendOpts {
|
||||||
method?: 'GET' | 'POST' | 'PATCH' | 'DELETE';
|
method?: 'GET' | 'POST' | 'PATCH' | 'DELETE';
|
||||||
body?: AnyJson;
|
body?: AnyJson;
|
||||||
@@ -119,29 +131,31 @@ export const brainApi = {
|
|||||||
|
|
||||||
/** Liste aller Memories, optional nach Type gefiltert. */
|
/** Liste aller Memories, optional nach Type gefiltert. */
|
||||||
listMemories(opts: { type?: string; limit?: number } = {}): Promise<Memory[]> {
|
listMemories(opts: { type?: string; limit?: number } = {}): Promise<Memory[]> {
|
||||||
const qs = new URLSearchParams();
|
const qs = _qs({ type: opts.type, limit: opts.limit || 500 });
|
||||||
if (opts.type) qs.set('type', opts.type);
|
return _send(`/memory/list${qs}`);
|
||||||
qs.set('limit', String(opts.limit || 500));
|
|
||||||
return _send(`/memory/list?${qs.toString()}`);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Volltext-Substring-Suche. */
|
/** Volltext-Substring-Suche. */
|
||||||
searchText(q: string, opts: { type?: string; includePinned?: boolean; k?: number } = {}): Promise<Memory[]> {
|
searchText(q: string, opts: { type?: string; includePinned?: boolean; k?: number } = {}): Promise<Memory[]> {
|
||||||
const qs = new URLSearchParams({ q });
|
const qs = _qs({
|
||||||
if (opts.type) qs.set('type', opts.type);
|
q,
|
||||||
qs.set('include_pinned', String(opts.includePinned !== false));
|
type: opts.type,
|
||||||
qs.set('k', String(opts.k || 50));
|
include_pinned: opts.includePinned !== false,
|
||||||
return _send(`/memory/search-text?${qs.toString()}`);
|
k: opts.k || 50,
|
||||||
|
});
|
||||||
|
return _send(`/memory/search-text${qs}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Semantische Suche (Embedder). */
|
/** Semantische Suche (Embedder). */
|
||||||
searchSemantic(q: string, opts: { type?: string; includePinned?: boolean; k?: number; threshold?: number } = {}): Promise<Memory[]> {
|
searchSemantic(q: string, opts: { type?: string; includePinned?: boolean; k?: number; threshold?: number } = {}): Promise<Memory[]> {
|
||||||
const qs = new URLSearchParams({ q });
|
const qs = _qs({
|
||||||
if (opts.type) qs.set('type', opts.type);
|
q,
|
||||||
qs.set('include_pinned', String(opts.includePinned !== false));
|
type: opts.type,
|
||||||
qs.set('k', String(opts.k || 10));
|
include_pinned: opts.includePinned !== false,
|
||||||
qs.set('score_threshold', String(opts.threshold ?? 0.30));
|
k: opts.k || 10,
|
||||||
return _send(`/memory/search?${qs.toString()}`);
|
score_threshold: opts.threshold ?? 0.30,
|
||||||
|
});
|
||||||
|
return _send(`/memory/search${qs}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Memory anlegen. */
|
/** Memory anlegen. */
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
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';
|
export const VERBOSE_LOGGING_KEY = 'aria_verbose_logging';
|
||||||
|
|
||||||
@@ -39,3 +41,77 @@ export function setVerboseLogging(verbose: boolean): void {
|
|||||||
applyState();
|
applyState();
|
||||||
AsyncStorage.setItem(VERBOSE_LOGGING_KEY, String(verbose)).catch(() => {});
|
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"))
|
logger.warning("[rvs] delete_message fehlgeschlagen: %s", result.get("error"))
|
||||||
return
|
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":
|
elif msg_type == "brain_request":
|
||||||
# Generischer RVS-Proxy fuer die Brain-HTTP-API.
|
# Generischer RVS-Proxy fuer die Brain-HTTP-API.
|
||||||
# payload: {requestId, method, path, body?, bodyBase64?, contentType?}
|
# payload: {requestId, method, path, body?, bodyBase64?, contentType?}
|
||||||
|
|||||||
@@ -1338,6 +1338,42 @@ const server = http.createServer((req, res) => {
|
|||||||
else broadcast({ type: "agent_activity", activity: "idle" });
|
else broadcast({ type: "agent_activity", activity: "idle" });
|
||||||
res.writeHead(200, { "Content-Type": "application/json" });
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
res.end(JSON.stringify({ ok: true }));
|
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") {
|
} else if (req.url === "/api/files-list" && req.method === "GET") {
|
||||||
// Liste alle Dateien in /shared/uploads/ — die kommen entweder vom User
|
// Liste alle Dateien in /shared/uploads/ — die kommen entweder vom User
|
||||||
// (Upload aus App/Diagnostic) oder von ARIA (aria_<name>.<ext> Pattern).
|
// (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",
|
"chat_history_request", "chat_history_response", "chat_cleared",
|
||||||
"delete_message_request", "chat_message_deleted",
|
"delete_message_request", "chat_message_deleted",
|
||||||
"brain_request", "brain_response",
|
"brain_request", "brain_response",
|
||||||
|
"app_log",
|
||||||
"file_delete_batch_request", "file_delete_batch_response",
|
"file_delete_batch_request", "file_delete_batch_response",
|
||||||
"file_zip_request", "file_zip_response",
|
"file_zip_request", "file_zip_response",
|
||||||
"xtts_delete_voice",
|
"xtts_delete_voice",
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# tools/
|
||||||
|
|
||||||
|
Hilfsskripte für die Dev-Maschine. Brauchen `.claude/aria-vm.env` (aus
|
||||||
|
`.example` kopieren + lokale VM-IP eintragen).
|
||||||
|
|
||||||
|
## fetch-app-logs.sh
|
||||||
|
|
||||||
|
Holt App-Crash-Logs von der VM und speichert sie unter `.aria-debug/`
|
||||||
|
(gitignored). Die App schickt JS-Errors und ungefangene Promise-
|
||||||
|
Rejections via RVS an die Bridge — Bridge sammelt in
|
||||||
|
`/shared/logs/app.log`, Diagnostic-Server gibt sie via
|
||||||
|
`GET /api/app-log` raus.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tools/fetch-app-logs.sh # 200 neueste Eintraege
|
||||||
|
tools/fetch-app-logs.sh --limit 50 # weniger
|
||||||
|
tools/fetch-app-logs.sh --watch # alle 5s pollen, neue rausgeben
|
||||||
|
tools/fetch-app-logs.sh --clear # nach Abholen Log auf VM leeren
|
||||||
|
```
|
||||||
|
|
||||||
|
Ausgabe enthaelt pro Eintrag: Uhrzeit, Level (error/warn/info), Scope
|
||||||
|
(z.B. `ChatScreen.InboxModal` oder `global-fatal`), Message, und die
|
||||||
|
ersten ~8 Stack-Frames. Die kompletten Daten liegen als JSON in
|
||||||
|
`.aria-debug/app-log-<timestamp>.json`.
|
||||||
|
|
||||||
|
Workflow nach einem Crash:
|
||||||
|
|
||||||
|
1. App rebuilden mit Crash-Reporting (passiert automatisch ab dem
|
||||||
|
`21a315c`-Commit)
|
||||||
|
2. Crash in der App ausloesen
|
||||||
|
3. `tools/fetch-app-logs.sh` auf der Dev-Maschine
|
||||||
|
4. Stacktrace lesen / Claude geben
|
||||||
|
5. Fix bauen
|
||||||
|
6. `tools/fetch-app-logs.sh --clear` damit der Log wieder sauber ist
|
||||||
Executable
+105
@@ -0,0 +1,105 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# fetch-app-logs.sh — App-Crash-Logs von der VM holen
|
||||||
|
#
|
||||||
|
# Nutzt .claude/aria-vm.env als Quelle fuer $ARIA_DIAG_URL und ruft
|
||||||
|
# GET /api/app-log?limit=N. Speichert die Roh-Response unter
|
||||||
|
# .aria-debug/app-log-<timestamp>.json und gibt eine kompakte
|
||||||
|
# Zusammenfassung auf stdout aus (letzte Eintraege mit Stack-Trace).
|
||||||
|
#
|
||||||
|
# Verwendung:
|
||||||
|
# tools/fetch-app-logs.sh # Default limit=200
|
||||||
|
# tools/fetch-app-logs.sh --limit 50 # nur 50 holen
|
||||||
|
# tools/fetch-app-logs.sh --clear # nach Abholen Log loeschen
|
||||||
|
# tools/fetch-app-logs.sh --watch # alle 5s pollen, neue Eintraege ausgeben
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
LIMIT=200
|
||||||
|
CLEAR=0
|
||||||
|
WATCH=0
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--limit) LIMIT="$2"; shift 2 ;;
|
||||||
|
--limit=*) LIMIT="${1#*=}"; shift ;;
|
||||||
|
--clear) CLEAR=1; shift ;;
|
||||||
|
--watch) WATCH=1; shift ;;
|
||||||
|
-h|--help)
|
||||||
|
sed -n '1,/^set/p' "$0" | sed '$d' | sed 's/^# \{0,1\}//'
|
||||||
|
exit 0 ;;
|
||||||
|
*) echo "Unbekannte Option: $1" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
ENV_FILE="$ROOT/.claude/aria-vm.env"
|
||||||
|
OUT_DIR="$ROOT/.aria-debug"
|
||||||
|
|
||||||
|
if [[ ! -f "$ENV_FILE" ]]; then
|
||||||
|
echo "FEHLER: $ENV_FILE nicht vorhanden. Aus .example kopieren und IP anpassen." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$ENV_FILE"
|
||||||
|
|
||||||
|
if [[ -z "${ARIA_DIAG_URL:-}" ]]; then
|
||||||
|
echo "FEHLER: ARIA_DIAG_URL nicht gesetzt in $ENV_FILE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$OUT_DIR"
|
||||||
|
|
||||||
|
fetch_once() {
|
||||||
|
local ts json file
|
||||||
|
ts="$(date +%Y%m%d_%H%M%S)"
|
||||||
|
json="$(curl -s --max-time 10 "${ARIA_DIAG_URL%/}/api/app-log?limit=$LIMIT")" || {
|
||||||
|
echo "FEHLER: curl gegen $ARIA_DIAG_URL fehlgeschlagen" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
file="$OUT_DIR/app-log-$ts.json"
|
||||||
|
echo "$json" > "$file"
|
||||||
|
python3 - "$file" <<'PY'
|
||||||
|
import json, sys
|
||||||
|
from pathlib import Path
|
||||||
|
data = json.loads(Path(sys.argv[1]).read_text())
|
||||||
|
entries = data.get("entries") or []
|
||||||
|
print(f"=== {len(entries)} Eintrag{'e' if len(entries)!=1 else ''} (gespeichert unter {sys.argv[1]}) ===")
|
||||||
|
for e in entries[-20:]:
|
||||||
|
ts = e.get("ts") or 0
|
||||||
|
from datetime import datetime
|
||||||
|
when = datetime.fromtimestamp(ts/1000).strftime("%H:%M:%S") if ts else "?"
|
||||||
|
lvl = e.get("level","?")
|
||||||
|
scope = e.get("scope","?")
|
||||||
|
msg = (e.get("message") or "").splitlines()[0][:200]
|
||||||
|
print(f"\n[{when}] {lvl:5} {scope}: {msg}")
|
||||||
|
stack = (e.get("stack") or "").strip()
|
||||||
|
if stack:
|
||||||
|
for line in stack.splitlines()[:8]:
|
||||||
|
print(f" {line}")
|
||||||
|
if len(stack.splitlines()) > 8:
|
||||||
|
print(f" ... ({len(stack.splitlines())-8} weitere Zeilen — siehe JSON)")
|
||||||
|
PY
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "$WATCH" == "1" ]]; then
|
||||||
|
echo "Watching $ARIA_DIAG_URL/api/app-log — Ctrl+C zum Beenden"
|
||||||
|
SEEN=""
|
||||||
|
while true; do
|
||||||
|
cur=$(curl -s --max-time 10 "${ARIA_DIAG_URL%/}/api/app-log?limit=$LIMIT") || cur=""
|
||||||
|
hash=$(echo "$cur" | md5sum | awk '{print $1}')
|
||||||
|
if [[ "$hash" != "$SEEN" && -n "$cur" ]]; then
|
||||||
|
SEEN="$hash"
|
||||||
|
fetch_once
|
||||||
|
fi
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
else
|
||||||
|
fetch_once
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$CLEAR" == "1" ]]; then
|
||||||
|
echo
|
||||||
|
echo "→ Log auf der VM leeren..."
|
||||||
|
curl -s --max-time 5 -X POST "${ARIA_DIAG_URL%/}/api/app-log/clear" | python3 -m json.tool || true
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user