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'; const store = createStore({ state: { isLoggedIn: localStorage.getItem('isLoggedIn') === 'true', connectionStatus: 'disconnected', // 'connected', 'connecting', 'disconnected', 'error' daemonConnectionStatus: 'disconnected', // 'connected', 'connecting', 'disconnected', 'error' user: JSON.parse(localStorage.getItem('user')) || null, // 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, user) { state.isLoggedIn = true; state.user = user; localStorage.setItem('isLoggedIn', 'true'); localStorage.setItem('user', JSON.stringify(user)); 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; localStorage.removeItem('isLoggedIn'); localStorage.removeItem('user'); 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 }, user) { await commit('dologin', user); 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'); // Socket.io URL für lokale Entwicklung und Produktion let socketIoUrl = import.meta.env.VITE_SOCKET_IO_URL || import.meta.env.VITE_API_BASE_URL; // Für lokale Entwicklung: direkte Backend-Verbindung if (!socketIoUrl && (import.meta.env.DEV || window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1')) { socketIoUrl = 'http://localhost:3001'; } // Normalisiere URL (Env-Variablen enthalten teils Pfade wie /api; Port kann absichtlich gesetzt sein, z.B. :4443) try { if (socketIoUrl) { const parsed = new URL(socketIoUrl, window.location.origin); // In Produktion: Verwende immer window.location.origin (Port 443), nicht den Port aus der Umgebungsvariable // Socket.io wird über Nginx-Proxy auf /socket.io/ weitergeleitet if (window.location.hostname === 'www.your-part.de' || window.location.hostname.includes('your-part.de')) { socketIoUrl = window.location.origin; } else { // Lokale Entwicklung: Origin aus parsed verwenden (inkl. Port) socketIoUrl = parsed.origin; } } else { // Fallback: aktuelle Origin verwenden socketIoUrl = window.location.origin; } } catch (e) { // Wenn Parsing fehlschlägt: letzte Rettung ist der aktuelle Origin try { socketIoUrl = window.location.origin; } catch (_) {} } const socket = io(socketIoUrl, { secure: true, transports: ['websocket', 'polling'] }); 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; console.log(`Backend-Reconnect-Versuch ${state.backendRetryCount + 1}/${maxRetries}`); if (state.backendRetryCount >= maxRetries) { // Nach maxRetries alle 5 Sekunden weiter versuchen console.log('Backend: Max Retries erreicht, versuche weiter alle 5 Sekunden...'); 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 console.log(`Backend: Warte ${delay}ms bis zum nächsten Reconnect-Versuch...`); 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 const hostname = window.location.hostname; const isLocalhost = hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1' || hostname === '[::1]'; const isProduction = hostname === 'www.your-part.de' || hostname.includes('your-part.de'); // Versuche Umgebungsvariable zu lesen (kann undefined sein, wenn nicht zur Build-Zeit gesetzt) let daemonUrl = import.meta.env?.VITE_DAEMON_SOCKET; console.log('[Daemon] Umgebungsvariable VITE_DAEMON_SOCKET:', daemonUrl); console.log('[Daemon] DEV-Modus:', import.meta.env?.DEV); console.log('[Daemon] Hostname:', hostname); console.log('[Daemon] IsLocalhost:', isLocalhost); console.log('[Daemon] IsProduction:', isProduction); // Wenn Umgebungsvariable nicht gesetzt ist oder leer, verwende Fallback-Logik if (!daemonUrl || (typeof daemonUrl === 'string' && daemonUrl.trim() === '')) { // Immer direkte Verbindung zum Daemon-Port 4551 (verschlüsselt) const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; daemonUrl = `${protocol}//${hostname}:4551/`; console.log('[Daemon] Verwende direkte Verbindung zu Port 4551'); } else { // Wenn Umgebungsvariable gesetzt ist, verwende sie direkt console.log('[Daemon] Verwende Umgebungsvariable:', daemonUrl); } console.log('[Daemon] Finale Daemon-URL:', daemonUrl); 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;