Ä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:
Torsten Schulz (local)
2025-09-15 08:45:11 +02:00
parent 8f4327efb5
commit d475e8b2f7
3 changed files with 188 additions and 7 deletions

View File

@@ -2,12 +2,40 @@
<header>
<div class="logo"><img src="/images/logos/logo.png" /></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>
</template>
<script>
import { mapGetters } from 'vuex';
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>
@@ -27,4 +55,69 @@ header {
.logo > img {
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>

View File

@@ -180,7 +180,11 @@ export default {
isPicking: false,
maxLightness: 92, // exclude extremely light colors (>% L)
// Reconnect control
reconnectIntervalMs: 5000,
reconnectIntervalMs: 3000,
reconnectAttempts: 0,
maxReconnectAttempts: 20,
heartbeatInterval: null,
heartbeatIntervalMs: 30000, // 30 seconds
connectAttemptTimeout: null,
joinFallbackTimer: null,
// Faster handshake watchdog separate from reconnect interval
@@ -258,6 +262,9 @@ export default {
this.opened = false;
console.log('[Chat WS] dialog close — closing websocket');
this.disconnectChatSocket();
// Remove network event listeners
window.removeEventListener('online', this.onOnline);
window.removeEventListener('offline', this.onOffline);
},
onOptionsToggle(e) {
e?.stopPropagation?.();
@@ -270,11 +277,18 @@ export default {
this.showOptions = false;
},
onOnline() {
console.log('[Chat WS] Network online detected');
if (this.opened && !this.chatConnected && !this.connectRacing && (!this.chatWs || this.chatWs.readyState !== WebSocket.OPEN)) {
console.log('[Chat WS] online — attempting reconnect');
this.reconnectAttempts = 0; // Reset attempts on network recovery
this.reconnectIntervalMs = 3000; // Reset to base interval
this.connectChatSocket();
}
},
onOffline() {
console.log('[Chat WS] Network offline detected');
this.setStatus('disconnected');
},
async loadRooms() {
try {
const data = await fetchPublicRooms();
@@ -315,6 +329,9 @@ export default {
// Stelle die WS-Verbindung her, wenn der Dialog geöffnet wird
this.opened = true;
this.connectChatSocket();
// Add network event listeners
window.addEventListener('online', this.onOnline);
window.addEventListener('offline', this.onOffline);
},
connectChatSocket() {
if (this.connectRacing) return; // avoid overlapping races
@@ -413,6 +430,9 @@ export default {
this.wsUrl = url;
this.chatWs = ws;
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);
// Close all other candidates
this.pendingWs.forEach(r => {
@@ -569,7 +589,21 @@ export default {
},
scheduleReconnect() {
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 = null;
if (this.opened && !this.chatConnected) {
@@ -594,6 +628,7 @@ export default {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
this.stopHeartbeat(); // Stop heartbeat when disconnecting
this.cleanupPendingSockets();
if (this.connectOpenTimer) { clearTimeout(this.connectOpenTimer); this.connectOpenTimer = null; }
if (this.connectAttemptTimeout) { clearTimeout(this.connectAttemptTimeout); this.connectAttemptTimeout = null; }
@@ -613,6 +648,35 @@ export default {
this.usersInRoom = [];
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() {
const r = this.rooms.find(x => x.id === this.selectedRoom);
return r?.title || r?.name || '';

View File

@@ -8,6 +8,7 @@ import { io } from 'socket.io-client';
const store = createStore({
state: {
isLoggedIn: localStorage.getItem('isLoggedIn') === 'true',
connectionStatus: 'disconnected', // 'connected', 'connecting', 'disconnected', 'error'
user: JSON.parse(localStorage.getItem('user')) || null,
language: navigator.language.startsWith('de') ? 'de' : 'en',
menu: JSON.parse(localStorage.getItem('menu')) || [],
@@ -45,6 +46,9 @@ const store = createStore({
setSocket(state, socket) {
state.socket = socket;
},
setConnectionStatus(state, status) {
state.connectionStatus = status;
},
clearSocket(state) {
if (state.socket) {
state.socket.disconnect();
@@ -86,6 +90,7 @@ const store = createStore({
if (currentSocket) {
currentSocket.disconnect();
}
commit('setConnectionStatus', 'connecting');
const socketIoUrl = import.meta.env.VITE_SOCKET_IO_URL || import.meta.env.VITE_API_BASE_URL;
console.log('🔌 Initializing Socket.io connection to:', socketIoUrl);
const socket = io(socketIoUrl, {
@@ -95,28 +100,40 @@ const store = createStore({
socket.on('connect', () => {
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;
if (idForSocket) socket.emit('setUserId', idForSocket);
});
socket.on('disconnect', (reason) => {
console.warn('❌ Socket.io disconnected:', reason);
commit('setConnectionStatus', 'disconnected');
retryConnection(connectSocket);
});
socket.on('connect_error', (error) => {
console.error('❌ Socket.io connection error:', error);
console.error('❌ URL attempted:', import.meta.env.VITE_API_BASE_URL);
commit('setConnectionStatus', 'error');
});
commit('setSocket', socket);
};
let retryCount = 0;
const maxRetries = 10;
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(() => {
console.log('Retrying Socket.io connection...');
reconnectFn();
}, 1000); // Retry every second
}, delay);
};
connectSocket();
@@ -236,14 +253,20 @@ const store = createStore({
};
let retryCount = 0;
const maxRetries = 5;
const maxRetries = 15; // Increased max retries
const retryConnection = (reconnectFn) => {
if (retryCount >= maxRetries) {
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;
}
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})`);
setTimeout(() => {
reconnectFn();
@@ -272,6 +295,7 @@ const store = createStore({
socket: state => state.socket,
daemonSocket: state => state.daemonSocket,
menuNeedsUpdate: state => state.menuNeedsUpdate,
connectionStatus: state => state.connectionStatus,
},
modules: {
dialogs,