Compare commits

...

2 Commits

Author SHA1 Message Date
duffyduck 7a66752655 release: bump version to 0.1.5.3 2026-05-15 22:33:10 +02:00
duffyduck b510ccd93a fix(app): Such-Reihenfolge + About-Escape + GPS-Heartbeat fuer near()
(1) Such-Treffer jetzt neueste zuerst (analog WhatsApp/Telegram). User
    ist visuell unten, der erste Sprung landet meist im Viewport ohne
    weiten Pre-Scroll (= weniger Cold-Start-Fail-Risiko). „Naechster"
    geht in die Vergangenheit. Plus Pre-Scroll-Wartezeit 80→200 ms damit
    FlatList beim ersten Versuch wirklich Zeit zum Rendern hat.

(2) SettingsScreen Ueber-Text: `—` wurde literal gerendert weil
    JSX-Text-Knoten keine JS-String-Escapes interpretieren. Fix:
    `{'—'}` als JS-Expression-Block.

(3) GPS-Tracking sendete nach der initialen Position nichts mehr wenn
    der User stationaer war — `distanceFilter: 30` blockiert
    watchPosition-Updates ohne Bewegung. Nach 5 min (NEAR_MAX_AGE_SEC)
    verwirft das Brain die Position als veraltet → near()-Watcher feuern
    nie. Stefan's DRK-Trigger waren so chronisch tot.

    Fix: zusaetzlich zum watchPosition laeuft ein setInterval(60s)
    Heartbeat der die zuletzt empfangene Position erneut sendet. Kein
    extra GPS-Wakeup — akkufreundlich. Damit bleibt der Brain-State
    frisch auch bei stationaerem User; near() funktioniert sobald der
    User tatsaechlich im Radius ist.

Anmerkung zu Stefan's konkretem Test: er war 1.5–2 km von den DRK-
Triggern entfernt (Radius je 300 m) — selbst mit frischen GPS-Updates
haetten die nicht gefeuert. Der Heartbeat-Fix ist trotzdem noetig
damit Trigger ueberhaupt eine Chance haben wenn er tatsaechlich dort
vorbeifaehrt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:30:51 +02:00
5 changed files with 39 additions and 10 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 10502 versionCode 10503
versionName "0.1.5.2" versionName "0.1.5.3"
// Fallback fuer Libraries mit Product Flavors // Fallback fuer Libraries mit Product Flavors
missingDimensionStrategy 'react-native-camera', 'general' missingDimensionStrategy 'react-native-camera', 'general'
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "aria-cockpit", "name": "aria-cockpit",
"version": "0.1.5.2", "version": "0.1.5.3",
"private": true, "private": true,
"scripts": { "scripts": {
"android": "react-native run-android", "android": "react-native run-android",
+11 -6
View File
@@ -1381,18 +1381,21 @@ const ChatScreen: React.FC = () => {
); );
const invertedMessages = useMemo(() => [...chatVisibleMessages].reverse(), [chatVisibleMessages]); const invertedMessages = useMemo(() => [...chatVisibleMessages].reverse(), [chatVisibleMessages]);
// Such-Treffer: alle Message-IDs die zur Query passen, in chronologischer // Such-Treffer: alle Message-IDs die zur Query passen. NEUESTE ZUERST —
// Reihenfolge (aelteste zuerst). Bei Query-Change resetten wir den Index. // analog zu WhatsApp/Telegram: User ist visuell unten im Chat, der erste
// Treffer ist meist schon im Viewport (kein weiter Pre-Scroll, kein
// Cold-Start-Sprung-Fail). „Naechster" geht in die Vergangenheit.
// WICHTIG: nur in chatVisibleMessages suchen — Spezial-Bubbles (Memory/ // WICHTIG: nur in chatVisibleMessages suchen — Spezial-Bubbles (Memory/
// Skill/Trigger) sind im Chat-Stream nicht sichtbar und Treffer auf die // Skill/Trigger) sind im Chat-Stream nicht sichtbar und Treffer auf die
// wuerden zu „ID nicht im FlatList → findIndex=-1 → kein Scroll"-Fail // wuerden zu „ID nicht im FlatList → findIndex=-1 → kein Scroll"-Fail
// fuehren (Cessna in einer Memory-Bubble → springt zur falschen Stelle). // fuehren.
const searchMatchIds = useMemo(() => { const searchMatchIds = useMemo(() => {
const q = searchQuery.trim().toLowerCase(); const q = searchQuery.trim().toLowerCase();
if (!q) return [] as string[]; if (!q) return [] as string[];
return chatVisibleMessages return chatVisibleMessages
.filter(m => (m.text || '').toLowerCase().includes(q)) .filter(m => (m.text || '').toLowerCase().includes(q))
.map(m => m.id); .map(m => m.id)
.reverse();
}, [chatVisibleMessages, searchQuery]); }, [chatVisibleMessages, searchQuery]);
useEffect(() => { useEffect(() => {
@@ -1470,7 +1473,9 @@ const ChatScreen: React.FC = () => {
animated: false, animated: false,
}); });
} catch {} } catch {}
// Nach kurzer Render-Pause praezise nachsetzen // Nach kurzer Render-Pause praezise nachsetzen. 200 ms statt 80 ms —
// bei Cold-Start braucht FlatList laenger fuer das Item-Layout, das
// war Stefans „erst beim zweiten Versuch klappt's"-Bug.
requestAnimationFrame(() => { requestAnimationFrame(() => {
setTimeout(() => { setTimeout(() => {
try { try {
@@ -1478,7 +1483,7 @@ const ChatScreen: React.FC = () => {
} catch { } catch {
// onScrollToIndexFailed-Handler uebernimmt den Fallback // onScrollToIndexFailed-Handler uebernimmt den Fallback
} }
}, 80); }, 200);
}); });
}, [searchIndex, searchMatchIds]); }, [searchIndex, searchMatchIds]);
+1 -1
View File
@@ -1798,7 +1798,7 @@ const SettingsScreen: React.FC = () => {
<Text style={styles.aboutTitle}>ARIA Cockpit</Text> <Text style={styles.aboutTitle}>ARIA Cockpit</Text>
<Text style={styles.aboutVersion}>Version {require('../../package.json').version}</Text> <Text style={styles.aboutVersion}>Version {require('../../package.json').version}</Text>
<Text style={styles.aboutInfo}> <Text style={styles.aboutInfo}>
ARIA \u2014 Autonomous Reasoning & Intelligence Assistant.{'\n'} ARIA {'\u2014'} Autonomous Reasoning & Intelligence Assistant.{'\n'}
Stefans Kommandozentrale.{'\n'} Stefans Kommandozentrale.{'\n'}
Gebaut mit React Native + TypeScript. Gebaut mit React Native + TypeScript.
</Text> </Text>
+24
View File
@@ -26,6 +26,13 @@ class GpsTrackingService {
private listeners: Set<Listener> = new Set(); private listeners: Set<Listener> = new Set();
// Defensive: nicht zu schnell oeffentlich togglen // Defensive: nicht zu schnell oeffentlich togglen
private lastChangeAt = 0; private lastChangeAt = 0;
// Letzte bekannte Position — wird vom Heartbeat-Timer alle 60s erneut
// an die Bridge gesendet, sonst veraltet near() im Brain (NEAR_MAX_AGE_SEC
// = 5 min) wenn der User stationaer ist und distanceFilter keine Updates
// mehr triggert.
private lastLat: number | null = null;
private lastLon: number | null = null;
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
isActive(): boolean { isActive(): boolean {
return this.active; return this.active;
@@ -84,6 +91,8 @@ class GpsTrackingService {
(pos) => { (pos) => {
const lat = pos.coords.latitude; const lat = pos.coords.latitude;
const lon = pos.coords.longitude; const lon = pos.coords.longitude;
this.lastLat = lat;
this.lastLon = lon;
rvs.send('location_update' as any, { lat, lon }); rvs.send('location_update' as any, { lat, lon });
}, },
(err) => { (err) => {
@@ -96,6 +105,17 @@ class GpsTrackingService {
fastestInterval: 10000, // (Android) max Frequenz fastestInterval: 10000, // (Android) max Frequenz
} as any, } as any,
); );
// Heartbeat: alle 60s die letzte bekannte Position erneut senden.
// Sonst bleibt der Brain-State stale wenn der User stationaer ist
// (distanceFilter blockt watchPosition-Updates) → near()-Watcher
// verwerfen die Position als veraltet (NEAR_MAX_AGE_SEC = 300s).
// Kein neuer GPS-Wakeup, nur Re-Send der letzten Werte → akkufreundlich.
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
this.heartbeatTimer = setInterval(() => {
if (this.lastLat != null && this.lastLon != null) {
rvs.send('location_update' as any, { lat: this.lastLat, lon: this.lastLon });
}
}, 60_000);
this.active = true; this.active = true;
this.lastChangeAt = Date.now(); this.lastChangeAt = Date.now();
this.notify(); this.notify();
@@ -118,6 +138,10 @@ class GpsTrackingService {
try { Geolocation.clearWatch(this.watchId); } catch {} try { Geolocation.clearWatch(this.watchId); } catch {}
this.watchId = null; this.watchId = null;
} }
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
this.active = false; this.active = false;
this.lastChangeAt = Date.now(); this.lastChangeAt = Date.now();
this.notify(); this.notify();