Änderung: Verbesserung der Verbindungsverwaltung und Benutzeroberfläche in mehreren Komponenten
Änderungen: - Hinzufügung eines Verbindungsstatus-Indicators in der AppHeader.vue, der den aktuellen Verbindungsstatus anzeigt. - Erweiterung der MultiChatDialog.vue um verbesserte Netzwerkereignisbehandlungen und eine Herzschlag-Logik zur Aufrechterhaltung der WebSocket-Verbindung. - Anpassungen im Store zur Verwaltung des Verbindungsstatus und zur Implementierung von Wiederverbindungslogik mit exponentiellem Backoff. - Diese Anpassungen verbessern die Benutzererfahrung durch klare Statusanzeigen und erhöhen die Stabilität der WebSocket-Verbindungen.
This commit is contained in:
@@ -2,12 +2,40 @@
|
|||||||
<header>
|
<header>
|
||||||
<div class="logo"><img src="/images/logos/logo.png" /></div>
|
<div class="logo"><img src="/images/logos/logo.png" /></div>
|
||||||
<div class="advertisement">Advertisement</div>
|
<div class="advertisement">Advertisement</div>
|
||||||
|
<div class="connection-status" v-if="isLoggedIn">
|
||||||
|
<div class="status-indicator" :class="connectionStatusClass">
|
||||||
|
<span class="status-dot"></span>
|
||||||
|
<span class="status-text">{{ connectionStatusText }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'AppHeader'
|
name: 'AppHeader',
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['isLoggedIn', 'connectionStatus']),
|
||||||
|
connectionStatusClass() {
|
||||||
|
return {
|
||||||
|
'status-connected': this.connectionStatus === 'connected',
|
||||||
|
'status-connecting': this.connectionStatus === 'connecting',
|
||||||
|
'status-disconnected': this.connectionStatus === 'disconnected',
|
||||||
|
'status-error': this.connectionStatus === 'error'
|
||||||
|
};
|
||||||
|
},
|
||||||
|
connectionStatusText() {
|
||||||
|
switch (this.connectionStatus) {
|
||||||
|
case 'connected': return 'Verbunden';
|
||||||
|
case 'connecting': return 'Verbinde...';
|
||||||
|
case 'disconnected': return 'Getrennt';
|
||||||
|
case 'error': return 'Fehler';
|
||||||
|
default: return 'Unbekannt';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -27,4 +55,69 @@ header {
|
|||||||
.logo > img {
|
.logo > img {
|
||||||
max-height: 50px;
|
max-height: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.connection-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 6px;
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-connected .status-dot {
|
||||||
|
background-color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-connecting .status-dot {
|
||||||
|
background-color: #ff9800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-disconnected .status-dot {
|
||||||
|
background-color: #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-error .status-dot {
|
||||||
|
background-color: #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-connected {
|
||||||
|
background-color: rgba(76, 175, 80, 0.1);
|
||||||
|
color: #2e7d32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-connecting {
|
||||||
|
background-color: rgba(255, 152, 0, 0.1);
|
||||||
|
color: #f57c00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-disconnected {
|
||||||
|
background-color: rgba(244, 67, 54, 0.1);
|
||||||
|
color: #d32f2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-error {
|
||||||
|
background-color: rgba(244, 67, 54, 0.1);
|
||||||
|
color: #d32f2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
|
100% { opacity: 1; }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -180,7 +180,11 @@ export default {
|
|||||||
isPicking: false,
|
isPicking: false,
|
||||||
maxLightness: 92, // exclude extremely light colors (>% L)
|
maxLightness: 92, // exclude extremely light colors (>% L)
|
||||||
// Reconnect control
|
// Reconnect control
|
||||||
reconnectIntervalMs: 5000,
|
reconnectIntervalMs: 3000,
|
||||||
|
reconnectAttempts: 0,
|
||||||
|
maxReconnectAttempts: 20,
|
||||||
|
heartbeatInterval: null,
|
||||||
|
heartbeatIntervalMs: 30000, // 30 seconds
|
||||||
connectAttemptTimeout: null,
|
connectAttemptTimeout: null,
|
||||||
joinFallbackTimer: null,
|
joinFallbackTimer: null,
|
||||||
// Faster handshake watchdog separate from reconnect interval
|
// Faster handshake watchdog separate from reconnect interval
|
||||||
@@ -258,6 +262,9 @@ export default {
|
|||||||
this.opened = false;
|
this.opened = false;
|
||||||
console.log('[Chat WS] dialog close — closing websocket');
|
console.log('[Chat WS] dialog close — closing websocket');
|
||||||
this.disconnectChatSocket();
|
this.disconnectChatSocket();
|
||||||
|
// Remove network event listeners
|
||||||
|
window.removeEventListener('online', this.onOnline);
|
||||||
|
window.removeEventListener('offline', this.onOffline);
|
||||||
},
|
},
|
||||||
onOptionsToggle(e) {
|
onOptionsToggle(e) {
|
||||||
e?.stopPropagation?.();
|
e?.stopPropagation?.();
|
||||||
@@ -270,11 +277,18 @@ export default {
|
|||||||
this.showOptions = false;
|
this.showOptions = false;
|
||||||
},
|
},
|
||||||
onOnline() {
|
onOnline() {
|
||||||
|
console.log('[Chat WS] Network online detected');
|
||||||
if (this.opened && !this.chatConnected && !this.connectRacing && (!this.chatWs || this.chatWs.readyState !== WebSocket.OPEN)) {
|
if (this.opened && !this.chatConnected && !this.connectRacing && (!this.chatWs || this.chatWs.readyState !== WebSocket.OPEN)) {
|
||||||
console.log('[Chat WS] online — attempting reconnect');
|
console.log('[Chat WS] online — attempting reconnect');
|
||||||
|
this.reconnectAttempts = 0; // Reset attempts on network recovery
|
||||||
|
this.reconnectIntervalMs = 3000; // Reset to base interval
|
||||||
this.connectChatSocket();
|
this.connectChatSocket();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onOffline() {
|
||||||
|
console.log('[Chat WS] Network offline detected');
|
||||||
|
this.setStatus('disconnected');
|
||||||
|
},
|
||||||
async loadRooms() {
|
async loadRooms() {
|
||||||
try {
|
try {
|
||||||
const data = await fetchPublicRooms();
|
const data = await fetchPublicRooms();
|
||||||
@@ -315,6 +329,9 @@ export default {
|
|||||||
// Stelle die WS-Verbindung her, wenn der Dialog geöffnet wird
|
// Stelle die WS-Verbindung her, wenn der Dialog geöffnet wird
|
||||||
this.opened = true;
|
this.opened = true;
|
||||||
this.connectChatSocket();
|
this.connectChatSocket();
|
||||||
|
// Add network event listeners
|
||||||
|
window.addEventListener('online', this.onOnline);
|
||||||
|
window.addEventListener('offline', this.onOffline);
|
||||||
},
|
},
|
||||||
connectChatSocket() {
|
connectChatSocket() {
|
||||||
if (this.connectRacing) return; // avoid overlapping races
|
if (this.connectRacing) return; // avoid overlapping races
|
||||||
@@ -413,6 +430,9 @@ export default {
|
|||||||
this.wsUrl = url;
|
this.wsUrl = url;
|
||||||
this.chatWs = ws;
|
this.chatWs = ws;
|
||||||
this.transportConnected = true;
|
this.transportConnected = true;
|
||||||
|
this.reconnectAttempts = 0; // Reset reconnect counter on successful connection
|
||||||
|
this.reconnectIntervalMs = 3000; // Reset to base interval
|
||||||
|
this.startHeartbeat(); // Start heartbeat to keep connection alive
|
||||||
console.log('[Chat WS] open in', dt, 'ms', '| protocol:', ws.protocol || '(none)', '| url:', url);
|
console.log('[Chat WS] open in', dt, 'ms', '| protocol:', ws.protocol || '(none)', '| url:', url);
|
||||||
// Close all other candidates
|
// Close all other candidates
|
||||||
this.pendingWs.forEach(r => {
|
this.pendingWs.forEach(r => {
|
||||||
@@ -569,7 +589,21 @@ export default {
|
|||||||
},
|
},
|
||||||
scheduleReconnect() {
|
scheduleReconnect() {
|
||||||
if (this.reconnectTimer) return;
|
if (this.reconnectTimer) return;
|
||||||
console.log('[Chat WS] scheduleReconnect in', this.reconnectIntervalMs, 'ms', '| opened:', this.opened);
|
|
||||||
|
// Check if we've exceeded max attempts
|
||||||
|
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||||
|
console.error('[Chat WS] Max reconnect attempts reached, waiting longer before retry');
|
||||||
|
this.reconnectAttempts = 0; // Reset counter
|
||||||
|
this.reconnectIntervalMs = 30000; // Wait 30 seconds
|
||||||
|
} else {
|
||||||
|
this.reconnectAttempts++;
|
||||||
|
// Use exponential backoff with jitter
|
||||||
|
const baseDelay = Math.min(3000 * Math.pow(1.5, this.reconnectAttempts - 1), 15000);
|
||||||
|
const jitter = Math.random() * 1000; // Add up to 1 second jitter
|
||||||
|
this.reconnectIntervalMs = baseDelay + jitter;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Chat WS] scheduleReconnect in', Math.round(this.reconnectIntervalMs), 'ms', '| opened:', this.opened, '| attempt:', this.reconnectAttempts);
|
||||||
this.reconnectTimer = setTimeout(() => {
|
this.reconnectTimer = setTimeout(() => {
|
||||||
this.reconnectTimer = null;
|
this.reconnectTimer = null;
|
||||||
if (this.opened && !this.chatConnected) {
|
if (this.opened && !this.chatConnected) {
|
||||||
@@ -594,6 +628,7 @@ export default {
|
|||||||
clearTimeout(this.reconnectTimer);
|
clearTimeout(this.reconnectTimer);
|
||||||
this.reconnectTimer = null;
|
this.reconnectTimer = null;
|
||||||
}
|
}
|
||||||
|
this.stopHeartbeat(); // Stop heartbeat when disconnecting
|
||||||
this.cleanupPendingSockets();
|
this.cleanupPendingSockets();
|
||||||
if (this.connectOpenTimer) { clearTimeout(this.connectOpenTimer); this.connectOpenTimer = null; }
|
if (this.connectOpenTimer) { clearTimeout(this.connectOpenTimer); this.connectOpenTimer = null; }
|
||||||
if (this.connectAttemptTimeout) { clearTimeout(this.connectAttemptTimeout); this.connectAttemptTimeout = null; }
|
if (this.connectAttemptTimeout) { clearTimeout(this.connectAttemptTimeout); this.connectAttemptTimeout = null; }
|
||||||
@@ -613,6 +648,35 @@ export default {
|
|||||||
this.usersInRoom = [];
|
this.usersInRoom = [];
|
||||||
this.selectedTargetUser = null;
|
this.selectedTargetUser = null;
|
||||||
},
|
},
|
||||||
|
startHeartbeat() {
|
||||||
|
this.stopHeartbeat(); // Clear any existing heartbeat
|
||||||
|
if (!this.opened || !this.chatConnected) return;
|
||||||
|
|
||||||
|
this.heartbeatInterval = setInterval(() => {
|
||||||
|
if (!this.chatWs || this.chatWs.readyState !== WebSocket.OPEN) {
|
||||||
|
console.warn('[Chat WS] Heartbeat failed - connection not open, attempting reconnect');
|
||||||
|
this.stopHeartbeat();
|
||||||
|
this.scheduleReconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Send a ping message to keep connection alive
|
||||||
|
this.wsSend({ type: 'ping' });
|
||||||
|
console.log('[Chat WS] Heartbeat sent');
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[Chat WS] Heartbeat failed:', error);
|
||||||
|
this.stopHeartbeat();
|
||||||
|
this.scheduleReconnect();
|
||||||
|
}
|
||||||
|
}, this.heartbeatIntervalMs);
|
||||||
|
},
|
||||||
|
stopHeartbeat() {
|
||||||
|
if (this.heartbeatInterval) {
|
||||||
|
clearInterval(this.heartbeatInterval);
|
||||||
|
this.heartbeatInterval = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
getSelectedRoomName() {
|
getSelectedRoomName() {
|
||||||
const r = this.rooms.find(x => x.id === this.selectedRoom);
|
const r = this.rooms.find(x => x.id === this.selectedRoom);
|
||||||
return r?.title || r?.name || '';
|
return r?.title || r?.name || '';
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { io } from 'socket.io-client';
|
|||||||
const store = createStore({
|
const store = createStore({
|
||||||
state: {
|
state: {
|
||||||
isLoggedIn: localStorage.getItem('isLoggedIn') === 'true',
|
isLoggedIn: localStorage.getItem('isLoggedIn') === 'true',
|
||||||
|
connectionStatus: 'disconnected', // 'connected', 'connecting', 'disconnected', 'error'
|
||||||
user: JSON.parse(localStorage.getItem('user')) || null,
|
user: JSON.parse(localStorage.getItem('user')) || null,
|
||||||
language: navigator.language.startsWith('de') ? 'de' : 'en',
|
language: navigator.language.startsWith('de') ? 'de' : 'en',
|
||||||
menu: JSON.parse(localStorage.getItem('menu')) || [],
|
menu: JSON.parse(localStorage.getItem('menu')) || [],
|
||||||
@@ -45,6 +46,9 @@ const store = createStore({
|
|||||||
setSocket(state, socket) {
|
setSocket(state, socket) {
|
||||||
state.socket = socket;
|
state.socket = socket;
|
||||||
},
|
},
|
||||||
|
setConnectionStatus(state, status) {
|
||||||
|
state.connectionStatus = status;
|
||||||
|
},
|
||||||
clearSocket(state) {
|
clearSocket(state) {
|
||||||
if (state.socket) {
|
if (state.socket) {
|
||||||
state.socket.disconnect();
|
state.socket.disconnect();
|
||||||
@@ -86,6 +90,7 @@ const store = createStore({
|
|||||||
if (currentSocket) {
|
if (currentSocket) {
|
||||||
currentSocket.disconnect();
|
currentSocket.disconnect();
|
||||||
}
|
}
|
||||||
|
commit('setConnectionStatus', 'connecting');
|
||||||
const socketIoUrl = import.meta.env.VITE_SOCKET_IO_URL || import.meta.env.VITE_API_BASE_URL;
|
const socketIoUrl = import.meta.env.VITE_SOCKET_IO_URL || import.meta.env.VITE_API_BASE_URL;
|
||||||
console.log('🔌 Initializing Socket.io connection to:', socketIoUrl);
|
console.log('🔌 Initializing Socket.io connection to:', socketIoUrl);
|
||||||
const socket = io(socketIoUrl, {
|
const socket = io(socketIoUrl, {
|
||||||
@@ -95,28 +100,40 @@ const store = createStore({
|
|||||||
|
|
||||||
socket.on('connect', () => {
|
socket.on('connect', () => {
|
||||||
console.log('✅ Socket.io connected successfully');
|
console.log('✅ Socket.io connected successfully');
|
||||||
|
retryCount = 0; // Reset retry counter on successful connection
|
||||||
|
commit('setConnectionStatus', 'connected');
|
||||||
const idForSocket = state.user?.hashedId || state.user?.id;
|
const idForSocket = state.user?.hashedId || state.user?.id;
|
||||||
if (idForSocket) socket.emit('setUserId', idForSocket);
|
if (idForSocket) socket.emit('setUserId', idForSocket);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('disconnect', (reason) => {
|
socket.on('disconnect', (reason) => {
|
||||||
console.warn('❌ Socket.io disconnected:', reason);
|
console.warn('❌ Socket.io disconnected:', reason);
|
||||||
|
commit('setConnectionStatus', 'disconnected');
|
||||||
retryConnection(connectSocket);
|
retryConnection(connectSocket);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('connect_error', (error) => {
|
socket.on('connect_error', (error) => {
|
||||||
console.error('❌ Socket.io connection error:', error);
|
console.error('❌ Socket.io connection error:', error);
|
||||||
console.error('❌ URL attempted:', import.meta.env.VITE_API_BASE_URL);
|
console.error('❌ URL attempted:', import.meta.env.VITE_API_BASE_URL);
|
||||||
|
commit('setConnectionStatus', 'error');
|
||||||
});
|
});
|
||||||
|
|
||||||
commit('setSocket', socket);
|
commit('setSocket', socket);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let retryCount = 0;
|
||||||
|
const maxRetries = 10;
|
||||||
const retryConnection = (reconnectFn) => {
|
const retryConnection = (reconnectFn) => {
|
||||||
|
if (retryCount >= maxRetries) {
|
||||||
|
console.error('❌ Max retry attempts reached for Socket.io');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
retryCount++;
|
||||||
|
const delay = Math.min(1000 * Math.pow(1.5, retryCount - 1), 30000); // Exponential backoff, max 30s
|
||||||
|
console.log(`🔄 Retrying Socket.io connection in ${delay}ms (attempt ${retryCount}/${maxRetries})`);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
console.log('Retrying Socket.io connection...');
|
|
||||||
reconnectFn();
|
reconnectFn();
|
||||||
}, 1000); // Retry every second
|
}, delay);
|
||||||
};
|
};
|
||||||
|
|
||||||
connectSocket();
|
connectSocket();
|
||||||
@@ -236,14 +253,20 @@ const store = createStore({
|
|||||||
};
|
};
|
||||||
|
|
||||||
let retryCount = 0;
|
let retryCount = 0;
|
||||||
const maxRetries = 5;
|
const maxRetries = 15; // Increased max retries
|
||||||
const retryConnection = (reconnectFn) => {
|
const retryConnection = (reconnectFn) => {
|
||||||
if (retryCount >= maxRetries) {
|
if (retryCount >= maxRetries) {
|
||||||
console.error('❌ Max retry attempts reached for Daemon WebSocket');
|
console.error('❌ Max retry attempts reached for Daemon WebSocket');
|
||||||
|
// Reset counter after a longer delay to allow for network recovery
|
||||||
|
setTimeout(() => {
|
||||||
|
retryCount = 0;
|
||||||
|
console.log('🔄 Resetting Daemon WebSocket retry counter - attempting reconnection');
|
||||||
|
reconnectFn();
|
||||||
|
}, 60000); // Wait 1 minute before resetting
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
retryCount++;
|
retryCount++;
|
||||||
const delay = Math.min(1000 * Math.pow(2, retryCount - 1), 10000); // Exponential backoff, max 10s
|
const delay = Math.min(1000 * Math.pow(1.5, retryCount - 1), 30000); // Exponential backoff, max 30s
|
||||||
console.log(`🔄 Retrying Daemon WebSocket connection in ${delay}ms (attempt ${retryCount}/${maxRetries})`);
|
console.log(`🔄 Retrying Daemon WebSocket connection in ${delay}ms (attempt ${retryCount}/${maxRetries})`);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
reconnectFn();
|
reconnectFn();
|
||||||
@@ -272,6 +295,7 @@ const store = createStore({
|
|||||||
socket: state => state.socket,
|
socket: state => state.socket,
|
||||||
daemonSocket: state => state.daemonSocket,
|
daemonSocket: state => state.daemonSocket,
|
||||||
menuNeedsUpdate: state => state.menuNeedsUpdate,
|
menuNeedsUpdate: state => state.menuNeedsUpdate,
|
||||||
|
connectionStatus: state => state.connectionStatus,
|
||||||
},
|
},
|
||||||
modules: {
|
modules: {
|
||||||
dialogs,
|
dialogs,
|
||||||
|
|||||||
Reference in New Issue
Block a user