Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e0cfef63c | |||
| b94626787b | |||
| ad87c807de |
+19
-1
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { PermissionsAndroid, Platform, StatusBar, StyleSheet } from 'react-native';
|
||||
import { AppState, AppStateStatus, PermissionsAndroid, Platform, StatusBar, StyleSheet } from 'react-native';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { NavigationContainer, DefaultTheme } from '@react-navigation/native';
|
||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||
@@ -107,8 +107,26 @@ const App: React.FC = () => {
|
||||
console.warn('[App] GPS-Tracking restore fehlgeschlagen:', err?.message || err);
|
||||
});
|
||||
|
||||
// AppState-Listener: nach Hintergrund-Rueckkehr aktiv die WS-
|
||||
// Verbindung neu aufbauen. Hintergrund: Android kann den TCP-Socket
|
||||
// im Background killen, JS-State zeigt aber noch OPEN → Stefan musste
|
||||
// manuell in Settings auf "Verbinden" tippen, oft mehrfach. Mit dem
|
||||
// force-Reconnect bei "active" greift das automatisch.
|
||||
let lastAppState: AppStateStatus = AppState.currentState;
|
||||
const appStateSub = AppState.addEventListener('change', (next) => {
|
||||
const wasBg = lastAppState !== 'active';
|
||||
lastAppState = next;
|
||||
if (next === 'active' && wasBg) {
|
||||
console.log('[App] Foreground-Resume — force-reconnect zum RVS');
|
||||
try { rvs.connect(true); } catch (e: any) {
|
||||
console.warn('[App] force-reconnect fehlgeschlagen:', e?.message || e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Beim Beenden: Verbindung sauber trennen
|
||||
return () => {
|
||||
appStateSub.remove();
|
||||
rvs.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -83,21 +83,39 @@ class RVSConnection {
|
||||
|
||||
// --- Verbindung ---
|
||||
|
||||
/** Verbindung zum RVS aufbauen */
|
||||
connect(): void {
|
||||
/** Verbindung zum RVS aufbauen. force=true: bestehende Connection hart
|
||||
* schliessen + neu verbinden (auch wenn JS denkt readyState=OPEN — kann
|
||||
* nach Hintergrund-Pause ein Zombie-WS sein wo TCP tot ist aber JS-State
|
||||
* noch OPEN zeigt; in dem Fall war "Bereits verbunden" ein No-Op und
|
||||
* Stefan musste manuell zigmal klicken). */
|
||||
connect(force: boolean = false): void {
|
||||
if (!this.config) {
|
||||
this.log('warn', 'Keine Verbindungskonfiguration vorhanden');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||
if (!force && this.ws?.readyState === WebSocket.OPEN) {
|
||||
this.log('info', 'Bereits verbunden');
|
||||
return;
|
||||
}
|
||||
|
||||
// Wenn ein WS-Objekt da ist (Zombie oder lebend), sauber abreissen
|
||||
// bevor wir einen neuen aufbauen — sonst gibt's zwei parallele
|
||||
// Verbindungen + doppelte Events.
|
||||
if (this.ws) {
|
||||
this.log('info', 'Bestehende WS-Verbindung wird geschlossen vor Neu-Connect');
|
||||
try {
|
||||
this.ws.onclose = null; // verhindert dass scheduleReconnect doppelt feuert
|
||||
this.ws.onerror = null;
|
||||
this.ws.close();
|
||||
} catch (_) {}
|
||||
this.ws = null;
|
||||
}
|
||||
|
||||
this.shouldReconnect = true;
|
||||
this.reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
|
||||
this.usingTLSFallback = false;
|
||||
this.clearTimers();
|
||||
this.log('info', `Verbindungsaufbau zu ${this.config.host}:${this.config.port} (TLS: ${this.config.useTLS ? 'ja' : 'nein'})`);
|
||||
this.establishConnection();
|
||||
}
|
||||
@@ -212,6 +230,16 @@ class RVSConnection {
|
||||
this.ws = null;
|
||||
this.setState('disconnected');
|
||||
|
||||
// Sticky-Fallback-Reset: beim naechsten Reconnect wieder primary
|
||||
// (wss://) versuchen statt fuer immer auf ws:// zu kleben. War
|
||||
// der Hauptgrund warum die App nach Hintergrund-Rueckkehr nicht
|
||||
// mehr verband — TLS-Handshake-Timeout in einem Reconnect → Fallback
|
||||
// auf ws:// → Caddy refused → endlos im Fallback haengen.
|
||||
if (this.usingTLSFallback) {
|
||||
this.log('info', 'Reset TLS-Fallback fuer naechsten Reconnect (zurueck zu wss://)');
|
||||
this.usingTLSFallback = false;
|
||||
}
|
||||
|
||||
if (this.shouldReconnect) {
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
|
||||
+43
-25
@@ -1650,36 +1650,54 @@
|
||||
if (msg.type === 'chat_history') {
|
||||
const boxes = [chatBox, document.getElementById('chat-box-fs')].filter(Boolean);
|
||||
for (const b of boxes) b.innerHTML = '';
|
||||
let errorCount = 0;
|
||||
if (msg.messages && msg.messages.length > 0) {
|
||||
for (const m of msg.messages) {
|
||||
if (m.type === 'aria_file') {
|
||||
// ARIA-Datei-Bubble — addAriaFile schreibt selbst in beide Boxen
|
||||
addAriaFile({ serverPath: m.serverPath, name: m.name, mimeType: m.mimeType, size: m.size, deleted: m.deleted });
|
||||
continue;
|
||||
}
|
||||
// [FILE: ...]-Marker rausfiltern (gleicher Filter wie addChat)
|
||||
const cleaned = (m.text || '').replace(/\[FILE:\s*\/shared\/uploads\/[^\]]+\]/gi, '').replace(/\n{3,}/g, '\n\n').trim();
|
||||
const escaped = escapeHtml(cleaned);
|
||||
let linked = linkifyText(escaped);
|
||||
// /shared/uploads/-Bildpfade auch im History inline rendern
|
||||
linked = linked.replace(/\/shared\/uploads\/[^\s<"]+\.(jpg|jpeg|png|gif|webp|svg|bmp)/gi, (match) => {
|
||||
return `<a href="${match}" target="_blank">${match}</a><img src="${match}" class="chat-media" onclick="openLightbox('image','${match}')" onerror="this.style.display='none'">`;
|
||||
});
|
||||
const time = m.ts ? new Date(m.ts).toLocaleTimeString('de-DE') : '?';
|
||||
const trashBtn = m.ts
|
||||
? `<button class="bubble-trash" title="Diese Bubble loeschen" onclick="deleteDiagBubble(${m.ts})">🗑</button>`
|
||||
: '';
|
||||
const innerHtml = `${trashBtn}${linked}<div class="meta">${escapeHtml(m.meta)} — ${time}</div>`;
|
||||
for (const b of boxes) {
|
||||
const el = document.createElement('div');
|
||||
el.className = `chat-msg ${m.type}`;
|
||||
if (m.ts) el.dataset.ts = String(m.ts);
|
||||
el.innerHTML = innerHtml;
|
||||
b.appendChild(el);
|
||||
for (let mi = 0; mi < msg.messages.length; mi++) {
|
||||
const m = msg.messages[mi];
|
||||
try {
|
||||
if (m.type === 'aria_file') {
|
||||
addAriaFile({ serverPath: m.serverPath, name: m.name, mimeType: m.mimeType, size: m.size, deleted: m.deleted });
|
||||
continue;
|
||||
}
|
||||
const cleaned = (m.text || '').replace(/\[FILE:\s*\/shared\/uploads\/[^\]]+\]/gi, '').replace(/\n{3,}/g, '\n\n').trim();
|
||||
const escaped = escapeHtml(cleaned);
|
||||
let linked = linkifyText(escaped);
|
||||
linked = linked.replace(/\/shared\/uploads\/[^\s<"]+\.(jpg|jpeg|png|gif|webp|svg|bmp)/gi, (match) => {
|
||||
return `<a href="${match}" target="_blank">${match}</a><img src="${match}" class="chat-media" onclick="openLightbox('image','${match}')" onerror="this.style.display='none'">`;
|
||||
});
|
||||
const time = m.ts ? new Date(m.ts).toLocaleTimeString('de-DE') : '?';
|
||||
const trashBtn = m.ts
|
||||
? `<button class="bubble-trash" title="Diese Bubble loeschen" onclick="deleteDiagBubble(${m.ts})">🗑</button>`
|
||||
: '';
|
||||
const innerHtml = `${trashBtn}${linked}<div class="meta">${escapeHtml(m.meta)} — ${time}</div>`;
|
||||
for (const b of boxes) {
|
||||
const el = document.createElement('div');
|
||||
el.className = `chat-msg ${m.type}`;
|
||||
if (m.ts) el.dataset.ts = String(m.ts);
|
||||
el.innerHTML = innerHtml;
|
||||
b.appendChild(el);
|
||||
}
|
||||
} catch (renderErr) {
|
||||
// Eine kaputte Bubble darf nicht den Rest der History killen.
|
||||
// Vorher passierte genau das: Frontend-Render bracht bei einer
|
||||
// problematischen Antwort ab, alle nachfolgenden Nachrichten waren
|
||||
// beim Reload weg. Jetzt: Fehler-Bubble einbauen + weitermachen.
|
||||
errorCount++;
|
||||
console.error('chat_history render error at idx ' + mi + ':', renderErr, m);
|
||||
for (const b of boxes) {
|
||||
const el = document.createElement('div');
|
||||
el.className = `chat-msg ${m.type || 'received'}`;
|
||||
if (m.ts) el.dataset.ts = String(m.ts);
|
||||
el.innerHTML = `<span style="color:#FF6B6B;">⚠ Render-Fehler in Bubble (${escapeHtml(String(renderErr.message || renderErr))})</span><div class="meta">${m.ts ? new Date(m.ts).toLocaleTimeString('de-DE') : '?'}</div>`;
|
||||
b.appendChild(el);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const b of boxes) b.scrollTop = b.scrollHeight;
|
||||
}
|
||||
if (errorCount > 0) {
|
||||
console.warn(`chat_history: ${errorCount} Bubble(s) konnten nicht gerendert werden`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+10
-2
@@ -701,8 +701,16 @@ function connectRVS(forcePlain) {
|
||||
state.rvs.lastError = err.message;
|
||||
broadcastState();
|
||||
|
||||
// TLS Fallback
|
||||
if (useTls && RVS_TLS_FALLBACK === "true" && !fallbackTriggered) {
|
||||
// TLS-Fallback nur bei wirklichen TLS/Handshake-Fehlern.
|
||||
// Bei Netz-Problemen wie EHOSTUNREACH, ECONNREFUSED, ENETUNREACH,
|
||||
// EAI_AGAIN ist der Server eh tot — Fallback bringt nichts ausser
|
||||
// Log-Spam und doppelten Retries.
|
||||
const netErr = (err.code || err.message || "").toString();
|
||||
const isNetDown =
|
||||
/^(EHOSTUNREACH|ECONNREFUSED|ENETUNREACH|ETIMEDOUT|EAI_AGAIN|ENOTFOUND)$/.test(netErr) ||
|
||||
/EHOSTUNREACH|ECONNREFUSED|ENETUNREACH|ETIMEDOUT|EAI_AGAIN|ENOTFOUND/.test(err.message || "");
|
||||
|
||||
if (useTls && RVS_TLS_FALLBACK === "true" && !fallbackTriggered && !isNetDown) {
|
||||
fallbackTriggered = true;
|
||||
log("warn", "rvs", "TLS fehlgeschlagen — Fallback auf ws://");
|
||||
try { ws.removeAllListeners(); ws.close(); } catch (_) {}
|
||||
|
||||
@@ -39,7 +39,7 @@ services:
|
||||
restart: always
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "444:443"
|
||||
command: caddy reverse-proxy --from ${PUBLIC_URL} --to rvs:3000
|
||||
volumes:
|
||||
- ./data/caddy/data:/data # Zertifikate (PERSISTENT)
|
||||
|
||||
Reference in New Issue
Block a user