Compare commits

...

9 Commits

Author SHA1 Message Date
duffyduck c2475ffef6 release: bump version to 0.1.6.9 2026-05-30 20:46:55 +02:00
duffyduck 98982fea2f feat(app): App-Logs live im Settings → Protokoll → Live Logs Tab anzeigen
Stefan: "wir haben live log + events tab in protokoll einstellungen, da
ist aber nie was drin".

Bisher hoerten Live Logs / Events nur auf RVS-Messages type='log'/'event'
von der Bridge — die Bridge schickt aktuell aber keine solchen Messages
zurueck zur App. Plus: reportAppDebug/Error ging nur an die Bridge in
/shared/logs/app.log, lokal in der App war nichts sichtbar.

Loesung: lokaler DeviceEventEmitter-Bus.

logger.ts:
- APP_LOG_EVENT Konstante exportiert
- reportAppError + reportAppDebug emittieren ZUSAETZLICH zum
  RVS-Send ein lokales DeviceEventEmitter-Event (errors immer,
  debug nur wenn Toggle AN)

SettingsScreen.tsx:
- DeviceEventEmitter.addListener auf APP_LOG_EVENT
- Mappt Log-Entries 1:1 in den 'logs'-State (max 200)
- Cleanup in useEffect-return

Damit sieht Stefan beim Debuggen (Debug-Toggle AN, Live-Logs-Tab
offen) live in der App was passiert — ohne curl gegen Bridge.

APK neu bauen erforderlich.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 20:44:42 +02:00
duffyduck 356f8b3171 feat(app): Debug-Logs-an-Bridge Toggle (Settings → Protokoll, default aus)
Stefan: "haben wir einen Menupunkt logging? sonst muellen wir uns dicht
wenns funktioniert und wir das logging im moment nicht brauchen"

Stimmt. reportAppDebug() schickt aktuell IMMER an Bridge, auch wenn
gar nicht debuggt wird. Bei armed Wake-Word + Pipeline-Logs sind das
schnell ein Dutzend Eintraege pro Wake-Trigger.

Loesung: separater Settings-Toggle "Debug-Logs an Bridge" mit eigenem
AsyncStorage-Key (aria_debug_logs_to_bridge), Default AUS.

- logger.ts: _debugLogsToBridge flag + isDebugLogsToBridge() /
  setDebugLogsToBridge(). initLogger() laedt den Wert. reportAppDebug()
  prueft das Flag und schickt nur wenn AN.
- SettingsScreen: neuer Toggle direkt unter Verbose-Logging,
  orange (#FF9500) damit er als "Power-User-Option" erkennbar ist,
  mit Erklaerungs-Hinweis dass nur Info-Logs gefiltert werden,
  Crash-Reports (Errors via reportAppError) gehen weiterhin IMMER.

Workflow:
- Default-User: Toggle aus, kein Traffic, kein Disk-Schreiben
- Stefan beim Debuggen: Toggle an, testet die App, schaut Logs via
  curl /api/app-log?lines=N, schaltet wieder aus

APK neu bauen erforderlich.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 20:41:40 +02:00
duffyduck b4115bb345 debug(wake): mehr Log-Punkte zwischen onWakeDetected-Trigger und Callback-Feuern
Stefan's Test zeigt: 'wake.detect keyword=computer state=armed' kommt
im Background durch (WakeLock greift!), aber 'wake.cb callback fired'
aus ChatScreen fehlt. Heisst: zwischen Detection und Callback-Feuern
geht's irgendwo verloren.

Mehr Logs:
- nach OpenWakeWord.stop(): 'native stop ok' oder 'native stop FAIL msg'
  → klaert ob async stop() haengt
- vor setTimeout: 'state→conversing, wakeCallbacks.length=N, scheduling'
  → klaert ob Liste leer ist (ChatScreen unmounted) und ob wir's
    schedulen
- im setTimeout: 'timeout fired, state=X, cbs=N'
  → klaert ob der Timer in 200ms tatsaechlich feuert (Doze-Throttle?)
- bei barge-path: 'barge path: cbs=N'

Damit sehen wir genau wo's klemmt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 20:38:14 +02:00
duffyduck 02cac99ef9 release: bump version to 0.1.6.8 2026-05-30 20:29:41 +02:00
duffyduck 2940ce0075 release: bump version to 0.1.6.7 2026-05-30 20:28:38 +02:00
duffyduck d78b668e31 feat(app): reportAppDebug — Live-Debug-Logs an Bridge ohne ADB
Stefan-Anforderung: Background-Wake-Word-Pipeline klappt noch nicht,
ADB nicht zur Hand → Debug via RVS-Log-Pipeline.

Logger:
- reportAppDebug(scope, message) analog zu reportAppError aber
  level=info, kein console.error, fuer Live-Diagnose

Strategische Log-Punkte:
- wakeword.ts: start() emits 'wake.start armed'
- wakeword.ts: onWakeDetected emits 'wake.detect state=X' beim
  Native-Trigger-Empfang
- ChatScreen.tsx wake-callback: 'wake.cb callback fired',
  'wake.cb startRecording=X', 'wake.cb gong played'
- backgroundAudio.ts: 'bg.start slot=X', 'bg.stop service stopped',
  'bg.start.fail msg' wenn Service nicht hochkommt

Abruf live via curl http://172.0.2.33:3001/api/app-log?lines=100

Damit kann Stefan nach APK-Build (mit allen Native-Fixes + Logger)
im Background-Test exakt sehen wo es klemmt:
- Kommt 'wake.detect' im Hintergrund an? (WakeLock-Frage)
- Kommt 'wake.cb callback fired'? (JS-Bridge-Frage)
- Geht 'bg.start slot=wake' durch? (Service-Start-Frage)

APK neu bauen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 20:27:29 +02:00
duffyduck a9115699db release: bump version to 0.1.6.6 2026-05-30 20:21:29 +02:00
duffyduck f2bfd4bbc6 feat(app): Background-GPS als opt-in Settings-Toggle
Stefan-Anforderung: GPS soll auch im Hintergrund liefern (Auto-Szenarien,
Handy-Tasche), aber NUR fuer Power-User die das bewusst aktivieren.
Mama-Tauglichkeit bleibt erhalten — Default AUS, keine Surprise-Permission.

Aenderungen:

AndroidManifest:
- ACCESS_BACKGROUND_LOCATION Permission
- FOREGROUND_SERVICE_LOCATION Permission
- AriaPlaybackService foregroundServiceType erweitert um |location
  (vorher: mediaPlayback|microphone)

backgroundAudio.ts:
- Neuer Slot 'location' zwischen 'wake' und 'background' in der
  Prioritaeten-Liste. Notification zeigt entsprechend.

gpsTracking.ts:
- isBackgroundGpsEnabled() / setBackgroundGpsEnabled() AsyncStorage-Helper
- ensureBackgroundLocationPermission() pruefte ACCESS_BACKGROUND_LOCATION
  und oeffnet Android-Settings wenn fehlend (auf Android 10+ kann das
  NICHT ueber den normalen Permission-Dialog angefordert werden)
- start(): wenn BG-GPS enabled, acquireBackgroundAudio('location') →
  Foreground-Service hochziehen mit type=location
- stop(): releaseBackgroundAudio('location')

SettingsScreen.tsx:
- Neuer Toggle "GPS auch im Hintergrund" direkt unter dem
  GPS-Tracking-Toggle, rot (#FF3B30) statt orange weil's eine stark
  privacy-relevante Einstellung ist
- Erklaerungs-Text zu Android-Settings + Akku-Verbrauch
- Beim Aktivieren: Permission-Check, ggf. Android-Settings oeffnen
- Wenn Tracking bereits laeuft: neustart damit location-Slot greift

APK neu bauen erforderlich.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 20:19:16 +02:00
9 changed files with 267 additions and 14 deletions
+2 -2
View File
@@ -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 10605 versionCode 10609
versionName "0.1.6.5" versionName "0.1.6.9"
// Fallback fuer Libraries mit Product Flavors // Fallback fuer Libraries mit Product Flavors
missingDimensionStrategy 'react-native-camera', 'general' missingDimensionStrategy 'react-native-camera', 'general'
} }
@@ -9,13 +9,20 @@
<!-- Optional: GPS-Position der Frage anhaengen (nur wenn User in Settings aktiviert) --> <!-- Optional: GPS-Position der Frage anhaengen (nur wenn User in Settings aktiviert) -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- Background-Location ist OPT-IN (Settings → GPS auch im Hintergrund).
Muss vom User explizit in Android-Einstellungen auf "Immer erlauben"
gesetzt werden — kann nicht ueber den normalen Permission-Dialog
angefordert werden (Android 10+). Default: aus. -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!-- Foreground-Service damit TTS auch bei minimierter App weiterlaeuft. <!-- Foreground-Service damit TTS auch bei minimierter App weiterlaeuft.
FOREGROUND_SERVICE_MICROPHONE ist Pflicht ab Android 14 wenn der FOREGROUND_SERVICE_MICROPHONE ist Pflicht ab Android 14 wenn der
Service waehrend des Backgrounds aufs Mikro zugreift (Wake-Word, Service waehrend des Backgrounds aufs Mikro zugreift (Wake-Word,
Aufnahme im Gespraechsmodus). --> Aufnahme im Gespraechsmodus). LOCATION wird nur aktiv wenn der
User Background-GPS in Settings einschaltet. -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- WAKE_LOCK damit Wake-Word + JS-Bridge auch bei aus-Display und Doze <!-- WAKE_LOCK damit Wake-Word + JS-Bridge auch bei aus-Display und Doze
arbeiten: ohne Lock pausiert Android die CPU, Native-AudioRecord arbeiten: ohne Lock pausiert Android die CPU, Native-AudioRecord
@@ -57,6 +64,6 @@
<service <service
android:name=".AriaPlaybackService" android:name=".AriaPlaybackService"
android:exported="false" android:exported="false"
android:foregroundServiceType="mediaPlayback|microphone" /> android:foregroundServiceType="mediaPlayback|microphone|location" />
</application> </application>
</manifest> </manifest>
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "aria-cockpit", "name": "aria-cockpit",
"version": "0.1.6.5", "version": "0.1.6.9",
"private": true, "private": true,
"scripts": { "scripts": {
"android": "react-native run-android", "android": "react-native run-android",
+3
View File
@@ -1270,9 +1270,11 @@ const ChatScreen: React.FC = () => {
useEffect(() => { useEffect(() => {
const unsubWake = wakeWordService.onWakeWord(async () => { const unsubWake = wakeWordService.onWakeWord(async () => {
console.log('[Chat] Gespraechsmodus — starte Auto-Aufnahme'); console.log('[Chat] Gespraechsmodus — starte Auto-Aufnahme');
import('../services/logger').then(m => m.reportAppDebug('wake.cb', 'callback fired, calling startRecording')).catch(()=>{});
// Conversation-Window: User hat X Sekunden um anzufangen, sonst Konversation aus // Conversation-Window: User hat X Sekunden um anzufangen, sonst Konversation aus
const windowMs = await loadConvWindowMs(); const windowMs = await loadConvWindowMs();
const started = await audioService.startRecording(true, windowMs); const started = await audioService.startRecording(true, windowMs);
import('../services/logger').then(m => m.reportAppDebug('wake.cb', `startRecording returned ${started}`)).catch(()=>{});
if (started) { if (started) {
// Erst JETZT signalisieren dass das Mikro wirklich offen ist — // Erst JETZT signalisieren dass das Mikro wirklich offen ist —
// vorher war's noch in der Init-Phase. So weiss der User exakt // vorher war's noch in der Init-Phase. So weiss der User exakt
@@ -1280,6 +1282,7 @@ const ChatScreen: React.FC = () => {
// ueber Settings → Wake-Word abschaltbar. // ueber Settings → Wake-Word abschaltbar.
ToastAndroid.show('🎤 Mikro offen — sprich jetzt', ToastAndroid.SHORT); ToastAndroid.show('🎤 Mikro offen — sprich jetzt', ToastAndroid.SHORT);
playWakeReadySound().catch(() => {}); playWakeReadySound().catch(() => {});
import('../services/logger').then(m => m.reportAppDebug('wake.cb', 'gong played + recording started')).catch(()=>{});
} else { } else {
// Mikrofon nicht verfuegbar, naechsten Versuch // Mikrofon nicht verfuegbar, naechsten Versuch
wakeWordService.resume(); wakeWordService.resume();
+92 -2
View File
@@ -20,6 +20,7 @@ import {
Modal, Modal,
PermissionsAndroid, PermissionsAndroid,
useWindowDimensions, useWindowDimensions,
DeviceEventEmitter,
} from 'react-native'; } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
import RNFS from 'react-native-fs'; import RNFS from 'react-native-fs';
@@ -52,13 +53,17 @@ import {
TTS_SPEED_STORAGE_KEY, TTS_SPEED_STORAGE_KEY,
} from '../services/audio'; } from '../services/audio';
import audioService from '../services/audio'; import audioService from '../services/audio';
import gpsTrackingService from '../services/gpsTracking'; import gpsTrackingService, {
isBackgroundGpsEnabled,
setBackgroundGpsEnabled,
ensureBackgroundLocationPermission,
} from '../services/gpsTracking';
import { acquireBackgroundAudio, releaseBackgroundAudio } from '../services/backgroundAudio'; import { acquireBackgroundAudio, releaseBackgroundAudio } from '../services/backgroundAudio';
import MemoryBrowser from '../components/MemoryBrowser'; import MemoryBrowser from '../components/MemoryBrowser';
import TriggerBrowser from '../components/TriggerBrowser'; import TriggerBrowser from '../components/TriggerBrowser';
import SkillBrowser from '../components/SkillBrowser'; import SkillBrowser from '../components/SkillBrowser';
import OAuthBrowser from '../components/OAuthBrowser'; import OAuthBrowser from '../components/OAuthBrowser';
import { isVerboseLogging, setVerboseLogging } from '../services/logger'; import { isVerboseLogging, setVerboseLogging, isDebugLogsToBridge, setDebugLogsToBridge, APP_LOG_EVENT } from '../services/logger';
import { import {
isWakeReadySoundEnabled, isWakeReadySoundEnabled,
setWakeReadySoundEnabled, setWakeReadySoundEnabled,
@@ -134,6 +139,7 @@ const SettingsScreen: React.FC = () => {
const [currentMode, setCurrentMode] = useState('normal'); const [currentMode, setCurrentMode] = useState('normal');
const [gpsEnabled, setGpsEnabled] = useState(false); const [gpsEnabled, setGpsEnabled] = useState(false);
const [gpsTracking, setGpsTracking] = useState(gpsTrackingService.isActive()); const [gpsTracking, setGpsTracking] = useState(gpsTrackingService.isActive());
const [bgGpsEnabled, setBgGpsEnabled] = useState(false);
const [backgroundMode, setBackgroundMode] = useState(true); // Default an const [backgroundMode, setBackgroundMode] = useState(true); // Default an
const [showSystemHints, setShowSystemHints] = useState(false); // Default aus const [showSystemHints, setShowSystemHints] = useState(false); // Default aus
const [scannerVisible, setScannerVisible] = useState(false); const [scannerVisible, setScannerVisible] = useState(false);
@@ -155,6 +161,7 @@ const SettingsScreen: React.FC = () => {
const [apkCacheInfo, setApkCacheInfo] = useState<{count: number, totalMB: number} | null>(null); const [apkCacheInfo, setApkCacheInfo] = useState<{count: number, totalMB: number} | null>(null);
const [ttsCacheInfo, setTtsCacheInfo] = useState<{count: number, totalMB: number} | null>(null); const [ttsCacheInfo, setTtsCacheInfo] = useState<{count: number, totalMB: number} | null>(null);
const [verboseLogging, setVerboseLoggingState] = useState<boolean>(isVerboseLogging()); const [verboseLogging, setVerboseLoggingState] = useState<boolean>(isVerboseLogging());
const [debugLogsToBridge, setDebugLogsToBridgeState] = useState<boolean>(isDebugLogsToBridge());
const [ttsSpeed, setTtsSpeed] = useState<number>(TTS_SPEED_DEFAULT); const [ttsSpeed, setTtsSpeed] = useState<number>(TTS_SPEED_DEFAULT);
const [wakeKeyword, setWakeKeyword] = useState<string>(DEFAULT_KEYWORD); const [wakeKeyword, setWakeKeyword] = useState<string>(DEFAULT_KEYWORD);
const [wakeStatus, setWakeStatus] = useState<string>(''); const [wakeStatus, setWakeStatus] = useState<string>('');
@@ -216,6 +223,8 @@ const SettingsScreen: React.FC = () => {
const offGps = gpsTrackingService.onChange(setGpsTracking); const offGps = gpsTrackingService.onChange(setGpsTracking);
// Persistierten Status wiederherstellen (war Tracking beim letzten Mal an?) // Persistierten Status wiederherstellen (war Tracking beim letzten Mal an?)
gpsTrackingService.restoreFromStorage().catch(() => {}); gpsTrackingService.restoreFromStorage().catch(() => {});
// Background-GPS-Toggle initial laden
isBackgroundGpsEnabled().then(setBgGpsEnabled).catch(() => {});
AsyncStorage.getItem(TTS_PREROLL_STORAGE_KEY).then(saved => { AsyncStorage.getItem(TTS_PREROLL_STORAGE_KEY).then(saved => {
if (saved != null) { if (saved != null) {
const n = parseFloat(saved); const n = parseFloat(saved);
@@ -380,6 +389,19 @@ const SettingsScreen: React.FC = () => {
setConnLog(prev => [...prev.slice(-99), entry]); setConnLog(prev => [...prev.slice(-99), entry]);
}); });
// Lokale App-Logs (reportAppDebug/Error) im Live-Logs-Tab anzeigen
// — damit Stefan ohne curl direkt in der App sieht was passiert.
const localLogSub = DeviceEventEmitter.addListener(APP_LOG_EVENT, (e: any) => {
const entry: LogEntry = {
id: `applog_${e.ts || Date.now()}_${logIdCounter++}`,
timestamp: e.ts || Date.now(),
source: e.scope || 'app',
message: e.message || '',
level: e.level || 'info',
};
setLogs(prev => [...prev.slice(-200), entry]);
});
const unsubMessage = rvs.onMessage((message: RVSMessage) => { const unsubMessage = rvs.onMessage((message: RVSMessage) => {
if (message.type === 'log') { if (message.type === 'log') {
const entry: LogEntry = { const entry: LogEntry = {
@@ -515,6 +537,7 @@ const SettingsScreen: React.FC = () => {
unsubState(); unsubState();
unsubMessage(); unsubMessage();
unsubLog(); unsubLog();
localLogSub.remove();
}; };
}, []); }, []);
@@ -1117,6 +1140,52 @@ const SettingsScreen: React.FC = () => {
thumbColor={gpsTracking ? '#FFFFFF' : '#666680'} thumbColor={gpsTracking ? '#FFFFFF' : '#666680'}
/> />
</View> </View>
{/* Background-GPS opt-in — Default AUS. Braucht ACCESS_BACKGROUND_LOCATION
(User muss in Android-Settings 'Immer erlauben' aktivieren). */}
<View style={[styles.toggleRow, {marginTop: 12, borderTopWidth: 1, borderTopColor: '#1E1E2E', paddingTop: 12}]}>
<View style={styles.toggleInfo}>
<Text style={styles.toggleLabel}>GPS auch im Hintergrund</Text>
<Text style={styles.toggleHint}>
Damit ARIA auch unterwegs deine aktuelle Position kennt wenn die
App im Hintergrund ist (Auto, Handy-Tasche). Standard: aus.
{'\n\n'}
Android verlangt fuer Background-GPS, dass du in den
System-Einstellungen unter Standort "Immer erlauben" auswaehlst.
Beim Aktivieren wird Android-Settings geoeffnet falls noetig.
{'\n\n'}
Akku-Verbrauch: ~3-5% mehr pro Tag durch dauerhaftes Polling.
</Text>
</View>
<Switch
value={bgGpsEnabled}
onValueChange={async (v) => {
if (v) {
const ok = await ensureBackgroundLocationPermission();
if (!ok) {
// User muss in Android-Settings auf "Immer erlauben" — Toggle
// bleibt aus bis er zurueckkommt und nochmal tippt.
return;
}
await setBackgroundGpsEnabled(true);
setBgGpsEnabled(true);
// Wenn Tracking bereits laeuft: neu starten damit der
// Foreground-Service jetzt mit location-Slot kommt
if (gpsTrackingService.isActive()) {
gpsTrackingService.stop('bg-toggle');
gpsTrackingService.start('bg-aktiviert').catch(() => {});
}
ToastAndroid.show('Background-GPS aktiviert', ToastAndroid.SHORT);
} else {
await setBackgroundGpsEnabled(false);
setBgGpsEnabled(false);
ToastAndroid.show('Background-GPS aus nur noch Foreground', ToastAndroid.SHORT);
}
}}
trackColor={{ false: '#2A2A3E', true: '#FF3B30' }}
thumbColor={bgGpsEnabled ? '#FFFFFF' : '#666680'}
/>
</View>
</View> </View>
{/* === Bubble-Anzeige === */} {/* === Bubble-Anzeige === */}
@@ -1863,6 +1932,27 @@ const SettingsScreen: React.FC = () => {
Warnungen und Fehler bleiben immer aktiv. Bei Bedarf einschalten zum Warnungen und Fehler bleiben immer aktiv. Bei Bedarf einschalten zum
Debuggen via adb logcat. Debuggen via adb logcat.
</Text> </Text>
{/* Debug-Logs an Bridge: scharf nur wenn aktiv gebraucht */}
<View style={[styles.toggleRow, {marginTop: 12, borderTopWidth: 1, borderTopColor: '#1E1E2E', paddingTop: 12}]}>
<Text style={styles.toggleLabel}>Debug-Logs an Bridge</Text>
<Switch
value={debugLogsToBridge}
onValueChange={(v) => {
setDebugLogsToBridge(v);
setDebugLogsToBridgeState(v);
}}
trackColor={{ false: '#3A3A52', true: '#FF9500' }}
thumbColor={debugLogsToBridge ? '#FFFFFF' : '#666680'}
/>
</View>
<Text style={styles.toggleHint}>
Schickt detaillierte Diagnose-Logs (Wake-Word-Pipeline, Audio-Focus,
Background-Service) per RVS an die Bridge abrufbar via
`curl /api/app-log?lines=N` ohne ADB. Default AUS damit kein
unnoetiger Traffic + Disk-Schreiben. Crash-Reports (Errors) gehen
IMMER, dieser Toggle betrifft nur Info-Logs.
</Text>
</View> </View>
<View style={styles.card}> <View style={styles.card}>
+7 -3
View File
@@ -9,13 +9,14 @@
* - 'tts' : ARIA spricht * - 'tts' : ARIA spricht
* - 'rec' : Aufnahme laeuft * - 'rec' : Aufnahme laeuft
* - 'wake' : Wake-Word lauscht passiv (Ohr aktiv) * - 'wake' : Wake-Word lauscht passiv (Ohr aktiv)
* - 'location' : Background-GPS-Tracking (opt-in in Settings)
* - 'background' : Persistenter Hintergrund-Modus (Settings-Toggle). * - 'background' : Persistenter Hintergrund-Modus (Settings-Toggle).
* Haelt JS-Engine + WebSocket auch ohne Audio am Leben * Haelt JS-Engine + WebSocket auch ohne Audio am Leben
* → Trigger-Replies, Reconnects, Push-Reaktionen. * → Trigger-Replies, Reconnects, Push-Reaktionen.
* *
* Solange mindestens ein Slot aktiv ist, laeuft der Service. Wenn alle * Solange mindestens ein Slot aktiv ist, laeuft der Service. Wenn alle
* Slots leer sind, wird er gestoppt. Der Notification-Text passt sich an * Slots leer sind, wird er gestoppt. Der Notification-Text passt sich an
* den hoechstprioren Slot an (tts > rec > wake > background). * den hoechstprioren Slot an (tts > rec > wake > location > background).
*/ */
import { NativeModules } from 'react-native'; import { NativeModules } from 'react-native';
@@ -27,13 +28,13 @@ interface BackgroundAudioNative {
const { BackgroundAudio } = NativeModules as { BackgroundAudio?: BackgroundAudioNative }; const { BackgroundAudio } = NativeModules as { BackgroundAudio?: BackgroundAudioNative };
type Slot = 'tts' | 'rec' | 'wake' | 'background'; type Slot = 'tts' | 'rec' | 'wake' | 'location' | 'background';
const slots = new Set<Slot>(); const slots = new Set<Slot>();
// Prioritaet fuer den Notification-Text — hoechste zuerst. 'background' // Prioritaet fuer den Notification-Text — hoechste zuerst. 'background'
// ist die fallback-Anzeige wenn nichts anderes laeuft. // ist die fallback-Anzeige wenn nichts anderes laeuft.
const PRIORITY: Slot[] = ['tts', 'rec', 'wake', 'background']; const PRIORITY: Slot[] = ['tts', 'rec', 'wake', 'location', 'background'];
function topReason(): string { function topReason(): string {
for (const s of PRIORITY) { for (const s of PRIORITY) {
@@ -47,6 +48,7 @@ async function applyState(): Promise<void> {
if (slots.size === 0) { if (slots.size === 0) {
try { await BackgroundAudio.stop(); } catch {} try { await BackgroundAudio.stop(); } catch {}
console.log('[BackgroundAudio] Service gestoppt (keine Slots)'); console.log('[BackgroundAudio] Service gestoppt (keine Slots)');
import('./logger').then(m => m.reportAppDebug('bg.stop', 'service stopped')).catch(()=>{});
return; return;
} }
const reason = topReason(); const reason = topReason();
@@ -54,8 +56,10 @@ async function applyState(): Promise<void> {
await BackgroundAudio.start(reason); await BackgroundAudio.start(reason);
console.log('[BackgroundAudio] Service aktiv (slot=%s, slots=%s)', console.log('[BackgroundAudio] Service aktiv (slot=%s, slots=%s)',
reason, [...slots].join('+')); reason, [...slots].join('+'));
import('./logger').then(m => m.reportAppDebug('bg.start', `slot=${reason} all=[${[...slots].join(',')}]`)).catch(()=>{});
} catch (err: any) { } catch (err: any) {
console.warn('[BackgroundAudio] start fehlgeschlagen:', err?.message || err); console.warn('[BackgroundAudio] start fehlgeschlagen:', err?.message || err);
import('./logger').then(m => m.reportAppDebug('bg.start.fail', err?.message || String(err))).catch(()=>{});
} }
} }
+64 -1
View File
@@ -14,9 +14,62 @@
*/ */
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
import { PermissionsAndroid, Platform, ToastAndroid } from 'react-native'; import { Linking, PermissionsAndroid, Platform, ToastAndroid } from 'react-native';
import Geolocation from '@react-native-community/geolocation'; import Geolocation from '@react-native-community/geolocation';
import rvs from './rvs'; import rvs from './rvs';
import { acquireBackgroundAudio, releaseBackgroundAudio } from './backgroundAudio';
// Opt-in Background-GPS — Settings-Toggle "GPS auch im Hintergrund".
// Default AUS. Wenn AN: ACCESS_BACKGROUND_LOCATION-Permission noetig
// (kann nicht ueber Standard-Dialog angefordert werden, User muss in
// Android-Settings auf "Immer erlauben" gehen) + ForegroundService mit
// foregroundServiceType=location wird hochgezogen.
export const BG_GPS_STORAGE_KEY = 'aria_gps_background_enabled';
export async function isBackgroundGpsEnabled(): Promise<boolean> {
try {
const v = await AsyncStorage.getItem(BG_GPS_STORAGE_KEY);
return v === 'true';
} catch {
return false;
}
}
export async function setBackgroundGpsEnabled(enabled: boolean): Promise<void> {
try {
await AsyncStorage.setItem(BG_GPS_STORAGE_KEY, String(enabled));
} catch {}
}
/** Prueft ob ACCESS_BACKGROUND_LOCATION gewaehrt ist und oeffnet sonst die
* Android-App-Settings damit der User "Immer erlauben" auswaehlen kann.
* Returns true wenn permission ok, false wenn User Settings oeffnen muss. */
export async function ensureBackgroundLocationPermission(): Promise<boolean> {
if (Platform.OS !== 'android') return true;
try {
const granted = await PermissionsAndroid.check(
'android.permission.ACCESS_BACKGROUND_LOCATION' as any,
);
if (granted) return true;
// Erst FINE_LOCATION anfordern falls noch nicht da
const fine = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
);
if (fine !== PermissionsAndroid.RESULTS.GRANTED) return false;
// Ab Android 10+ kann BACKGROUND_LOCATION NICHT ueber den normalen
// PermissionsAndroid.request abgefragt werden — User muss in Settings
// auf "Immer erlauben" wechseln. Wir oeffnen die App-Settings-Seite.
ToastAndroid.show(
'Bitte in Android-Einstellungen unter Standort "Immer erlauben" auswaehlen',
ToastAndroid.LONG,
);
Linking.openSettings();
return false;
} catch (e) {
console.warn('[gps-track] BG-Permission-Check fehlgeschlagen:', e);
return false;
}
}
type Listener = (active: boolean) => void; type Listener = (active: boolean) => void;
@@ -86,6 +139,14 @@ class GpsTrackingService {
ToastAndroid.show('GPS-Tracking: Berechtigung abgelehnt', ToastAndroid.LONG); ToastAndroid.show('GPS-Tracking: Berechtigung abgelehnt', ToastAndroid.LONG);
return false; return false;
} }
// Background-GPS opt-in: wenn aktiv, ForegroundService mit type=location
// hochziehen. Brauche ACCESS_BACKGROUND_LOCATION (User muss in Android-
// Settings 'Immer erlauben' aktivieren). Wenn die fehlt, watchPosition
// liefert im Hintergrund keine Updates (nur Heartbeat sendet alte Werte).
const bgEnabled = await isBackgroundGpsEnabled();
if (bgEnabled) {
try { await acquireBackgroundAudio('location'); } catch {}
}
try { try {
this.watchId = Geolocation.watchPosition( this.watchId = Geolocation.watchPosition(
(pos) => { (pos) => {
@@ -142,6 +203,8 @@ class GpsTrackingService {
clearInterval(this.heartbeatTimer); clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null; this.heartbeatTimer = null;
} }
// Location-Foreground-Service-Slot freigeben (falls vorher acquired)
try { releaseBackgroundAudio('location'); } catch {}
this.active = false; this.active = false;
this.lastChangeAt = Date.now(); this.lastChangeAt = Date.now();
this.notify(); this.notify();
+73 -2
View File
@@ -7,10 +7,28 @@
*/ */
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
import { Platform } from 'react-native'; import { Platform, DeviceEventEmitter } from 'react-native';
import rvs from './rvs'; import rvs from './rvs';
// Lokales Event damit die SettingsScreen Live Logs / Events Tabs
// auch das sehen was die App SELBST loggt (reportAppDebug/Error).
// Bisher gingen die nur via RVS an die Bridge. Lokal sichtbar = Mama-
// tauglich Debug ohne curl.
export const APP_LOG_EVENT = 'AriaLocalAppLog';
interface LocalLogEntry {
ts: number;
level: 'info' | 'warn' | 'error';
scope: string;
message: string;
}
export const VERBOSE_LOGGING_KEY = 'aria_verbose_logging'; export const VERBOSE_LOGGING_KEY = 'aria_verbose_logging';
// Eigener Toggle fuer Debug-Logs die ueber RVS an die Bridge gehen
// (/shared/logs/app.log → Diagnostic /api/app-log). Damit der Default-User
// nicht stuendlich Traffic + Disk-Schreiben hat, dieser ist DEFAULT AUS.
// Stefan schaltet's nur ein wenn er ein konkretes Problem debuggen muss.
export const DEBUG_LOGS_TO_BRIDGE_KEY = 'aria_debug_logs_to_bridge';
// Original-console.log retten, damit wir die Wrapper jederzeit wieder // Original-console.log retten, damit wir die Wrapper jederzeit wieder
// "scharf" stellen koennen (sonst waere ein Toggle-an nach -aus tot). // "scharf" stellen koennen (sonst waere ein Toggle-an nach -aus tot).
@@ -18,6 +36,7 @@ const originalLog = console.log.bind(console);
const noop = () => {}; const noop = () => {};
let _verbose = true; let _verbose = true;
let _debugLogsToBridge = false;
function applyState(): void { function applyState(): void {
console.log = _verbose ? originalLog : noop; console.log = _verbose ? originalLog : noop;
@@ -29,6 +48,10 @@ export async function initLogger(): Promise<void> {
const v = await AsyncStorage.getItem(VERBOSE_LOGGING_KEY); const v = await AsyncStorage.getItem(VERBOSE_LOGGING_KEY);
_verbose = v !== 'false'; // default: true _verbose = v !== 'false'; // default: true
} catch {} } catch {}
try {
const d = await AsyncStorage.getItem(DEBUG_LOGS_TO_BRIDGE_KEY);
_debugLogsToBridge = d === 'true'; // default: false
} catch {}
applyState(); applyState();
} }
@@ -42,6 +65,15 @@ export function setVerboseLogging(verbose: boolean): void {
AsyncStorage.setItem(VERBOSE_LOGGING_KEY, String(verbose)).catch(() => {}); AsyncStorage.setItem(VERBOSE_LOGGING_KEY, String(verbose)).catch(() => {});
} }
export function isDebugLogsToBridge(): boolean {
return _debugLogsToBridge;
}
export function setDebugLogsToBridge(enabled: boolean): void {
_debugLogsToBridge = enabled;
AsyncStorage.setItem(DEBUG_LOGS_TO_BRIDGE_KEY, String(enabled)).catch(() => {});
}
// ─── App-Crash-Reporting via RVS ──────────────────────────────────── // ─── App-Crash-Reporting via RVS ────────────────────────────────────
// //
// Wenn die App crasht — egal ob React-Render-Fehler (ErrorBoundary) oder // Wenn die App crasht — egal ob React-Render-Fehler (ErrorBoundary) oder
@@ -61,9 +93,10 @@ let _reportingInstalled = false;
/** Schickt einen App-Fehler via RVS an die Bridge. */ /** Schickt einen App-Fehler via RVS an die Bridge. */
export function reportAppError(ev: AppErrorEvent): void { export function reportAppError(ev: AppErrorEvent): void {
const ts = Date.now();
try { try {
rvs.send('app_log' as any, { rvs.send('app_log' as any, {
ts: Date.now(), ts,
platform: Platform.OS, platform: Platform.OS,
level: ev.level || 'error', level: ev.level || 'error',
scope: ev.scope, scope: ev.scope,
@@ -73,11 +106,49 @@ export function reportAppError(ev: AppErrorEvent): void {
} catch { } catch {
// RVS noch nicht connected — Fehler geht im console weiter. // RVS noch nicht connected — Fehler geht im console weiter.
} }
// Lokal in den App-Logs-Tab emitten — Errors gehen IMMER durch
// (unabhaengig vom Debug-Toggle).
try {
const entry: LocalLogEntry = {
ts, level: ev.level || 'error', scope: ev.scope, message: ev.message,
};
DeviceEventEmitter.emit(APP_LOG_EVENT, entry);
} catch {}
// Plus lokal: console.error, damit Stefan's adb (wenn doch mal verfuegbar) // Plus lokal: console.error, damit Stefan's adb (wenn doch mal verfuegbar)
// den Crash sieht. // den Crash sieht.
console.error(`[app-error scope=${ev.scope}]`, ev.message, '\n', ev.stack || ''); console.error(`[app-error scope=${ev.scope}]`, ev.message, '\n', ev.stack || '');
} }
/** Schickt eine Debug-/Info-Message via RVS an die Bridge. Landet ebenfalls
* in /shared/logs/app.log — abrufbar via `curl /api/app-log?lines=N`.
* Im Gegensatz zu reportAppError: keine Stacktrace, level=info, kein
* console.error. Fuer Live-Diagnose im Hintergrund wenn ADB nicht da ist.
*
* Nur aktiv wenn Settings → Protokoll → Debug-Logs an Bridge AN ist.
* Default aus damit Mama-Modus keine Disk-Schreiblast hat. Error-Reports
* (reportAppError) gehen weiterhin IMMER durch. */
export function reportAppDebug(scope: string, message: string): void {
if (!_debugLogsToBridge) return;
const ts = Date.now();
const trimmed = String(message).slice(0, 2000);
try {
rvs.send('app_log' as any, {
ts,
platform: Platform.OS,
level: 'info',
scope,
message: trimmed,
});
} catch {}
// Plus lokal in den App-Logs-Tab emitten — damit Stefan in der App
// selbst (Settings → Protokoll → Live Logs) sieht was passiert,
// ohne curl gegen Bridge.
try {
const entry: LocalLogEntry = { ts, level: 'info', scope, message: trimmed };
DeviceEventEmitter.emit(APP_LOG_EVENT, entry);
} catch {}
}
/** Installiert einen globalen JS-Error-Handler der ungefangene Errors via /** Installiert einen globalen JS-Error-Handler der ungefangene Errors via
* RVS an die Bridge schickt. Beim App-Start aufrufen. */ * RVS an die Bridge schickt. Beim App-Start aufrufen. */
export function installGlobalCrashReporter(): void { export function installGlobalCrashReporter(): void {
+16 -1
View File
@@ -179,6 +179,8 @@ class WakeWordService {
try { try {
await OpenWakeWord.start(); await OpenWakeWord.start();
console.log('[WakeWord] armed — warte auf "%s"', this.keyword); console.log('[WakeWord] armed — warte auf "%s"', this.keyword);
// Debug-Log via RVS damit wir auch ohne ADB sehen wann es greift
import('./logger').then(m => m.reportAppDebug('wake.start', `armed, keyword=${this.keyword}`)).catch(()=>{});
ToastAndroid.show(`Lausche auf "${KEYWORD_LABELS[this.keyword]}"`, ToastAndroid.SHORT); ToastAndroid.show(`Lausche auf "${KEYWORD_LABELS[this.keyword]}"`, ToastAndroid.SHORT);
this.setState('armed'); this.setState('armed');
return true; return true;
@@ -236,15 +238,24 @@ class WakeWordService {
} }
console.log('[WakeWord] Wake-Word "%s" erkannt! (state=%s, barge=%s)', console.log('[WakeWord] Wake-Word "%s" erkannt! (state=%s, barge=%s)',
this.keyword, this.state, this.bargeListening); this.keyword, this.state, this.bargeListening);
import('./logger').then(m => m.reportAppDebug('wake.detect',
`keyword=${this.keyword} state=${this.state} barge=${this.bargeListening}`)).catch(()=>{});
this.lastTriggerAt = now; this.lastTriggerAt = now;
if (this.nativeReady && OpenWakeWord) { if (this.nativeReady && OpenWakeWord) {
try { await OpenWakeWord.stop(); } catch {} try {
await OpenWakeWord.stop();
import('./logger').then(m => m.reportAppDebug('wake.detect', 'native stop ok')).catch(()=>{});
} catch (e: any) {
import('./logger').then(m => m.reportAppDebug('wake.detect', `native stop FAIL ${e?.message}`)).catch(()=>{});
}
} }
this.bargeListening = false; this.bargeListening = false;
// Wenn wir bereits in 'conversing' sind und der Trigger waehrend ARIAs TTS // Wenn wir bereits in 'conversing' sind und der Trigger waehrend ARIAs TTS
// kam (Barge-In via Wake-Word), feuern wir einen separaten Callback damit // kam (Barge-In via Wake-Word), feuern wir einen separaten Callback damit
// ChatScreen das TTS abbrechen + neue Aufnahme starten kann. Sonst normal. // ChatScreen das TTS abbrechen + neue Aufnahme starten kann. Sonst normal.
if (this.state === 'conversing') { if (this.state === 'conversing') {
import('./logger').then(m => m.reportAppDebug('wake.detect',
`barge path: cbs=${this.bargeCallbacks.length}`)).catch(()=>{});
this.bargeCallbacks.forEach(cb => { this.bargeCallbacks.forEach(cb => {
try { cb(); } catch (e) { console.warn('[WakeWord] barge cb err:', e); } try { cb(); } catch (e) { console.warn('[WakeWord] barge cb err:', e); }
}); });
@@ -252,7 +263,11 @@ class WakeWordService {
return; return;
} }
this.setState('conversing'); this.setState('conversing');
import('./logger').then(m => m.reportAppDebug('wake.detect',
`state→conversing, wakeCallbacks.length=${this.wakeCallbacks.length}, scheduling 200ms timeout`)).catch(()=>{});
setTimeout(() => { setTimeout(() => {
import('./logger').then(m => m.reportAppDebug('wake.detect',
`timeout fired, state=${this.state}, cbs=${this.wakeCallbacks.length}`)).catch(()=>{});
if (this.state === 'conversing') { if (this.state === 'conversing') {
this.wakeCallbacks.forEach(cb => cb()); this.wakeCallbacks.forEach(cb => cb());
} }