import { createStore } from 'vuex'; import dialogs from './modules/dialogs'; import loadMenu from '../utils/menuLoader.js'; import router from '../router'; import apiClient from '../utils/axios.js'; import { io } from 'socket.io-client'; import { getDaemonSocketUrl, getSocketIoUrl } from '../utils/appConfig.js'; const AUTH_KEYS = ['isLoggedIn', 'user', 'userid']; function getStoredValue(key) { return localStorage.getItem(key) ?? sessionStorage.getItem(key); } function getStoredUser() { const storedUser = getStoredValue('user'); if (!storedUser) return null; try { return JSON.parse(storedUser); } catch (_) { return null; } } function clearAuthStorage() { AUTH_KEYS.forEach((key) => { localStorage.removeItem(key); sessionStorage.removeItem(key); }); } function persistAuthStorage(user, rememberMe) { const targetStorage = rememberMe ? localStorage : sessionStorage; clearAuthStorage(); targetStorage.setItem('isLoggedIn', 'true'); targetStorage.setItem('user', JSON.stringify(user)); targetStorage.setItem('userid', user?.id || ''); } const store = createStore({ state: { isLoggedIn: getStoredValue('isLoggedIn') === 'true', connectionStatus: 'disconnected', // 'connected', 'connecting', 'disconnected', 'error' daemonConnectionStatus: 'disconnected', // 'connected', 'connecting', 'disconnected', 'error' user: getStoredUser(), // Reconnect state management backendRetryCount: 0, daemonRetryCount: 0, backendRetryTimer: null, daemonRetryTimer: null, backendConnecting: false, daemonConnecting: false, language: (() => { // Verwende die gleiche Logik wie in main.js const browserLanguage = navigator.language || navigator.languages[0]; const germanSpeakingCountries = ['de', 'at', 'ch', 'li']; if (browserLanguage.startsWith('de')) { return 'de'; } const allLanguages = navigator.languages || [navigator.language]; for (const lang of allLanguages) { if (lang.startsWith('de-')) { const countryCode = lang.split('-')[1]?.toLowerCase(); if (germanSpeakingCountries.includes(countryCode)) { return 'de'; } } if (lang.startsWith('de_')) { const countryCode = lang.split('_')[1]?.toLowerCase(); if (germanSpeakingCountries.includes(countryCode)) { return 'de'; } } } return 'en'; })(), menu: JSON.parse(localStorage.getItem('menu')) || [], socket: null, daemonSocket: null, menuNeedsUpdate: false, }, mutations: { async dologin(state, payload) { const loginPayload = payload?.user ? payload : { user: payload, rememberMe: true }; const { user, rememberMe = true } = loginPayload; state.isLoggedIn = true; state.user = user; persistAuthStorage(user, rememberMe); state.menuNeedsUpdate = true; if (user.param.filter(param => ['birthdate', 'gender'].includes(param.name)).length < 2) { router.push({ path: '/settings/personal' }); } }, async dologout(state) { state.isLoggedIn = false; state.user = null; clearAuthStorage(); localStorage.removeItem('menu'); state.menuNeedsUpdate = false; // Setze die Sprache auf die Browser-Sprache zurück const browserLanguage = navigator.language || navigator.languages[0]; const germanSpeakingCountries = ['de', 'at', 'ch', 'li']; if (browserLanguage.startsWith('de')) { state.language = 'de'; } else { const allLanguages = navigator.languages || [navigator.language]; let isGerman = false; for (const lang of allLanguages) { if (lang.startsWith('de-')) { const countryCode = lang.split('-')[1]?.toLowerCase(); if (germanSpeakingCountries.includes(countryCode)) { isGerman = true; break; } } if (lang.startsWith('de_')) { const countryCode = lang.split('_')[1]?.toLowerCase(); if (germanSpeakingCountries.includes(countryCode)) { isGerman = true; break; } } } state.language = isGerman ? 'de' : 'en'; } }, setLanguage(state, language) { state.language = language; }, setMenu(state, menu) { state.menu = menu; localStorage.setItem('menu', JSON.stringify(menu)); state.menuNeedsUpdate = false; }, setSocket(state, socket) { state.socket = socket; }, setConnectionStatus(state, status) { state.connectionStatus = status; }, setDaemonConnectionStatus(state, status) { state.daemonConnectionStatus = status; }, clearSocket(state) { if (state.socket) { state.socket.disconnect(); } state.socket = null; // Cleanup retry timer if (state.backendRetryTimer) { clearTimeout(state.backendRetryTimer); state.backendRetryTimer = null; } state.backendConnecting = false; }, setDaemonSocket(state, daemonSocket) { state.daemonSocket = daemonSocket; }, clearDaemonSocket(state) { if (state.daemonSocket) { state.daemonSocket.close(); } state.daemonSocket = null; state.daemonConnectionStatus = 'disconnected'; // Cleanup retry timer if (state.daemonRetryTimer) { clearTimeout(state.daemonRetryTimer); state.daemonRetryTimer = null; } state.daemonConnecting = false; }, }, actions: { async login({ commit, dispatch }, payload) { await commit('dologin', payload); await dispatch('initializeSocket'); await dispatch('initializeDaemonSocket'); const socket = this.getters.socket; if (socket) { const idForSocket = user?.hashedId || user?.id; if (idForSocket) socket.emit('setUserId', idForSocket); } await dispatch('loadMenu'); }, logout({ commit }) { commit('clearSocket'); commit('clearDaemonSocket'); commit('dologout'); router.push('/'); }, initializeSocket({ commit, state, dispatch }) { if (!state.isLoggedIn || !state.user || state.backendConnecting) { return; } state.backendConnecting = true; const connectSocket = () => { // Cleanup existing socket and timer if (state.socket) { state.socket.disconnect(); } if (state.backendRetryTimer) { clearTimeout(state.backendRetryTimer); state.backendRetryTimer = null; } commit('setConnectionStatus', 'connecting'); let socketIoUrl = getSocketIoUrl(); // Socket.io-Konfiguration: In Produktion mit HTTPS verwenden wir wss:// const socketOptions = { transports: ['websocket', 'polling'] }; // Wenn HTTPS verwendet wird, muss secure: true gesetzt sein if (socketIoUrl.startsWith('https://')) { socketOptions.secure = true; } const socket = io(socketIoUrl, socketOptions); socket.on('connect', () => { state.backendRetryCount = 0; // Reset retry counter on successful connection state.backendConnecting = false; commit('setConnectionStatus', 'connected'); const idForSocket = state.user?.hashedId || state.user?.id; if (idForSocket) socket.emit('setUserId', idForSocket); }); socket.on('disconnect', (reason) => { commit('setConnectionStatus', 'disconnected'); dispatch('retryBackendConnection'); }); socket.on('connect_error', (error) => { commit('setConnectionStatus', 'error'); dispatch('retryBackendConnection'); }); commit('setSocket', socket); }; connectSocket(); }, retryBackendConnection({ commit, state }) { if (state.backendRetryTimer || state.backendConnecting) { return; // Already retrying or connecting } const maxRetries = 10; if (state.backendRetryCount >= maxRetries) { // Nach maxRetries alle 5 Sekunden weiter versuchen state.backendRetryTimer = setTimeout(() => { state.backendRetryCount = 0; // Reset für nächsten Zyklus state.backendRetryTimer = null; commit('setConnectionStatus', 'connecting'); // Recursive call to retry setTimeout(() => this.dispatch('retryBackendConnection'), 100); }, 5000); return; } state.backendRetryCount++; const delay = 5000; // Alle 5 Sekunden versuchen state.backendRetryTimer = setTimeout(() => { state.backendRetryTimer = null; this.dispatch('initializeSocket'); }, delay); }, initializeDaemonSocket({ commit, state, dispatch }) { if (!state.isLoggedIn || !state.user || state.daemonConnecting) { return; } state.daemonConnecting = true; // Daemon URL für lokale Entwicklung und Produktion // Vite bindet Umgebungsvariablen zur Build-Zeit ein, daher Fallback-Logik basierend auf Hostname let daemonUrl = getDaemonSocketUrl(); const connectDaemonSocket = () => { // Cleanup existing socket and timer if (state.daemonSocket) { state.daemonSocket.close(); } if (state.daemonRetryTimer) { clearTimeout(state.daemonRetryTimer); state.daemonRetryTimer = null; } // Protokoll-Fallback: zuerst mit Subprotokoll, dann ohne const protocols = ['yourpart-protocol', undefined]; let attemptIndex = 0; const tryConnectWithProtocol = () => { const currentProtocol = protocols[attemptIndex]; try { commit('setDaemonConnectionStatus', 'connecting'); const daemonSocket = currentProtocol ? new WebSocket(daemonUrl, currentProtocol) : new WebSocket(daemonUrl); let opened = false; daemonSocket.onopen = () => { opened = true; state.daemonRetryCount = 0; state.daemonConnecting = false; if (state.daemonRetryTimer) { clearTimeout(state.daemonRetryTimer); state.daemonRetryTimer = null; } commit('setDaemonConnectionStatus', 'connected'); // Warte kurz, bevor setUserId gesendet wird, damit der Daemon bereit ist // Close-Code 1006 deutet darauf hin, dass der Daemon die Verbindung schließt, // möglicherweise weil setUserId zu früh gesendet wird setTimeout(() => { try { if (daemonSocket.readyState === WebSocket.OPEN && state.isLoggedIn && state.user) { const payload = JSON.stringify({ event: 'setUserId', data: { userId: state.user.id } }); daemonSocket.send(payload); console.log('[Daemon] setUserId gesendet für User:', state.user.id); } else { console.warn('[Daemon] Socket nicht mehr offen oder Benutzer nicht eingeloggt beim Senden von setUserId'); } } catch (error) { console.error('[Daemon] Fehler beim Senden von setUserId:', error); } }, 100); // 100ms Delay }; daemonSocket.onclose = (event) => { state.daemonConnecting = false; commit('setDaemonConnectionStatus', 'disconnected'); // Detailliertes Logging für Close-Events const closeCodeMessages = { 1000: 'Normal closure', 1001: 'Going away', 1002: 'Protocol error', 1003: 'Unsupported data', 1006: 'Abnormal closure (no close frame received)', 1007: 'Invalid data', 1008: 'Policy violation', 1009: 'Message too big', 1010: 'Extension error', 1011: 'Internal server error' }; const closeMessage = closeCodeMessages[event.code] || `Unknown code: ${event.code}`; console.warn(`[Daemon] Verbindung geschlossen - Code: ${event.code} (${closeMessage}), Reason: ${event.reason || 'none'}, WasClean: ${event.wasClean}, Opened: ${opened}`); // Bereinige Socket-Referenz wenn Verbindung geschlossen wurde if (state.daemonSocket === daemonSocket) { state.daemonSocket = null; } if (!opened && attemptIndex < protocols.length - 1) { attemptIndex += 1; tryConnectWithProtocol(); return; } // Nur reconnen, wenn Benutzer noch eingeloggt ist if (state.isLoggedIn && state.user) { dispatch('retryDaemonConnection'); } }; daemonSocket.onerror = (error) => { state.daemonConnecting = false; commit('setDaemonConnectionStatus', 'error'); console.error('[Daemon] WebSocket-Fehler:', error, 'ReadyState:', daemonSocket.readyState, 'URL:', daemonUrl); // Detaillierte Fehlermeldung für häufige Probleme if (daemonSocket.readyState === WebSocket.CLOSED) { const urlObj = new URL(daemonUrl); console.error(`[Daemon] Verbindung fehlgeschlagen zu ${urlObj.hostname}:${urlObj.port}`); console.error('[Daemon] Mögliche Ursachen:'); console.error(' - Daemon-Server läuft nicht auf diesem Port'); console.error(' - Port ist durch Firewall blockiert'); console.error(' - Falscher Port in VITE_DAEMON_SOCKET'); console.error(` - Aktuell konfiguriert: ${daemonUrl}`); } // Bereinige Socket-Referenz bei Fehler if (state.daemonSocket === daemonSocket) { state.daemonSocket = null; } if (!opened && attemptIndex < protocols.length - 1) { attemptIndex += 1; tryConnectWithProtocol(); return; } // Nur reconnen, wenn Benutzer noch eingeloggt ist if (state.isLoggedIn && state.user) { dispatch('retryDaemonConnection'); } }; daemonSocket.addEventListener('message', (event) => { const message = event.data; if (message === "ping") { daemonSocket.send("pong"); } else { try { const data = JSON.parse(message); // Handle daemon messages here } catch (error) { // Error parsing daemon message } } }); commit('setDaemonSocket', daemonSocket); } catch (error) { state.daemonConnecting = false; if (attemptIndex < protocols.length - 1) { attemptIndex += 1; tryConnectWithProtocol(); return; } dispatch('retryDaemonConnection'); } }; tryConnectWithProtocol(); }; connectDaemonSocket(); }, retryDaemonConnection({ commit, state, dispatch }) { // Prüfe ob Benutzer noch eingeloggt ist if (!state.isLoggedIn || !state.user) { console.log('[Daemon] Benutzer nicht eingeloggt, keine Wiederherstellung der Verbindung'); return; } // Prüfe ob bereits ein Timer läuft if (state.daemonRetryTimer) { return; } // Maximale Anzahl von Versuchen: 15 const maxRetries = 15; if (state.daemonRetryCount >= maxRetries) { console.warn(`[Daemon] Maximale Anzahl von Reconnect-Versuchen (${maxRetries}) erreicht.`); console.warn('[Daemon] Bitte prüfen Sie:'); console.warn(' 1. Läuft der Daemon-Server?'); console.warn(' 2. Ist der Port in VITE_DAEMON_SOCKET korrekt?'); console.warn(' 3. Ist der Port durch Firewall/Netzwerk erreichbar?'); console.warn(` 4. Aktuell konfiguriert: ${import.meta.env?.VITE_DAEMON_SOCKET || 'nicht gesetzt'}`); // Warte länger nach maximalen Versuchen (30 Sekunden) state.daemonRetryCount = 0; // Reset für nächsten Zyklus const delay = 30000; state.daemonRetryTimer = setTimeout(() => { state.daemonRetryTimer = null; commit('setDaemonConnectionStatus', 'connecting'); dispatch('initializeDaemonSocket'); }, delay); return; } state.daemonConnecting = false; state.daemonRetryCount++; const delay = 5000; console.log(`[Daemon] Reconnect-Versuch ${state.daemonRetryCount}/${maxRetries}, nächster Versuch in ${delay}ms...`); state.daemonRetryTimer = setTimeout(() => { state.daemonRetryTimer = null; // Prüfe noch einmal, ob Benutzer noch eingeloggt ist if (state.isLoggedIn && state.user) { commit('setDaemonConnectionStatus', 'connecting'); dispatch('initializeDaemonSocket'); } }, delay); }, setLanguage({ commit }, language) { commit('setLanguage', language); }, async loadMenu({ commit }) { try { const menu = await loadMenu(); commit('setMenu', menu); } catch (err) { commit('setMenu', []); } }, }, getters: { isLoggedIn: state => state.isLoggedIn, user: state => state.user, language: state => state.language, menu: state => state.menu, socket: state => state.socket, daemonSocket: state => state.daemonSocket, menuNeedsUpdate: state => state.menuNeedsUpdate, connectionStatus: state => state.connectionStatus, daemonConnectionStatus: state => state.daemonConnectionStatus, }, modules: { dialogs, }, }); // Initialisierung beim Laden der Anwendung - nur einmal ausführen if (store.state.isLoggedIn && store.state.user && !store.state.backendConnecting && !store.state.daemonConnecting) { store.dispatch('initializeSocket'); store.dispatch('initializeDaemonSocket'); } export default store;