fix(app): kompletter Server-Sync bei Reconnect — Server ist Source of Truth

Symptom: Diagnostic-Server hat leere Chat-History (z.B. nach "Konversation
zuruecksetzen" oder Wipe), App zeigt aber weiterhin ihren alten lokalen
Stand. Wer das Wipe-Event verpasst hat (App offline), bleibt veraltet.

Ursache: App schickte beim Reconnect chat_history_request {since: lastSync}
und ignorierte leere Antworten. Wenn der Server ueberhaupt nichts mehr hat
liefert er korrekt [] zurueck — App behielt aber lokalen State.

Fix:
  - App schickt jetzt {since: 0, limit: 200} → KOMPLETTER Server-Stand
  - Handler ersetzt die persistierte Chat-History mit dem Server-Stand
    (statt zu mergen)
  - Lokal-only Bubbles bleiben erhalten:
      * Skill-Created-Notifications (skillCreated gesetzt)
      * Laufende Sprachnachrichten ohne STT-Result (audioRequestId gesetzt
        und text leer/Placeholder)
  - Wenn Server leer: lastSync ebenfalls geloescht (sauberer Restart-State)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-11 23:55:25 +02:00
parent 8491fb2af7
commit 3497aa23f8
+30 -17
View File
@@ -407,15 +407,17 @@ const ChatScreen: React.FC = () => {
return; return;
} }
// chat_history_response: verpasste Nachrichten nachladen (bei Reconnect) // chat_history_response: kompletter Server-Stand. App ersetzt ihre
// persistierte Chat-History damit. Lokal-only Bubbles (laufende
// Voice-Aufnahmen ohne STT-Result, Skill-Created-Events ohne
// text) bleiben erhalten — die sind durch fehlendes 'text' oder
// skillCreated/audioRequestId klar als "lokal" erkennbar.
if (message.type === 'chat_history_response') { if (message.type === 'chat_history_response') {
const p = (message.payload || {}) as any; const p = (message.payload || {}) as any;
const incoming = (p.messages || []) as Array<any>; const incoming = (p.messages || []) as Array<any>;
if (!incoming.length) return; console.log(`[Chat] Server-Sync: ${incoming.length} Nachrichten vom Server`);
console.log(`[Chat] ${incoming.length} verpasste Nachrichten nachgeladen`); const fromServer: ChatMessage[] = incoming.map(m => {
const toAdd: ChatMessage[] = incoming.map(m => {
const role = m.role === 'user' ? 'user' : 'aria'; const role = m.role === 'user' ? 'user' : 'aria';
// ARIA-File-Marker aus dem Backup als attachments rekonstruieren
const files = Array.isArray(m.files) ? m.files : []; const files = Array.isArray(m.files) ? m.files : [];
const attachments = files.map((f: any) => ({ const attachments = files.map((f: any) => ({
type: (typeof f.mimeType === 'string' && f.mimeType.startsWith('image/')) ? 'image' : 'file', type: (typeof f.mimeType === 'string' && f.mimeType.startsWith('image/')) ? 'image' : 'file',
@@ -434,12 +436,24 @@ const ChatScreen: React.FC = () => {
}); });
const maxTs = incoming.reduce((mx: number, m: any) => Math.max(mx, m.ts || 0), 0); const maxTs = incoming.reduce((mx: number, m: any) => Math.max(mx, m.ts || 0), 0);
setMessages(prev => { setMessages(prev => {
// Dedup auf ts-basis: nicht erneut adden wenn schon was bei +/- 1s vorhanden // Lokal-only Bubbles erkennen + behalten:
const existingTs = new Set(prev.map(m => m.timestamp)); // - Skill-Created-Notifications (skillCreated gesetzt)
const newOnes = toAdd.filter(m => !existingTs.has(m.timestamp)); // - Laufende Sprachnachrichten ohne STT-Result (audioRequestId
return capMessages([...prev, ...newOnes]); // gesetzt UND text leer/Placeholder)
const localOnly = prev.filter(m =>
m.skillCreated ||
(m.audioRequestId && (!m.text || m.text === '🎙 Aufnahme...' || m.text === 'Aufnahme...'))
);
// Server-Stand + lokal-only (chronologisch sortiert)
const merged = [...fromServer, ...localOnly].sort((a, b) => a.timestamp - b.timestamp);
return capMessages(merged);
}); });
if (maxTs > 0) AsyncStorage.setItem('aria_chat_last_sync', String(maxTs)).catch(() => {}); if (maxTs > 0) {
AsyncStorage.setItem('aria_chat_last_sync', String(maxTs)).catch(() => {});
} else {
// Server leer → unsere lastSync auch zuruecksetzen
AsyncStorage.removeItem('aria_chat_last_sync').catch(() => {});
}
return; return;
} }
@@ -701,14 +715,13 @@ const ChatScreen: React.FC = () => {
const unsubState = rvs.onStateChange((state) => { const unsubState = rvs.onStateChange((state) => {
setConnectionState(state); setConnectionState(state);
// Bei (re)connect: verpasste Chat-Eintraege seit der letzten gesehenen // Bei (re)connect: KOMPLETTEN Server-Stand holen. Server ist die
// Nachricht abholen. lastChatSync wird beim Eingang von Nachrichten // Source-of-Truth — wenn er leer ist (z.B. nach "Konversation
// hochgezaehlt; default 0 = alle (gecappt auf Server-Limit). // zuruecksetzen"), soll die App das spiegeln, auch wenn sie offline
// war als das passiert ist. since=0 + limit=200 → die letzten 200
// Nachrichten vom Server, oder leeres Array wenn Server leer.
if (state === 'connected') { if (state === 'connected') {
AsyncStorage.getItem('aria_chat_last_sync').then(stored => { rvs.send('chat_history_request' as any, { since: 0, limit: 200 });
const since = stored ? parseInt(stored, 10) || 0 : 0;
rvs.send('chat_history_request' as any, { since, limit: 100 });
}).catch(() => {});
} }
}); });