Files
yourpart3/frontend/src/store/index.js
Torsten Schulz (local) 0cc280ed55 Refactor WebSocket and API configurations in yourpart-https.conf and yourpart-websocket-fixed.conf
- Removed outdated WebSocket handling from yourpart-https.conf for improved clarity.
- Updated yourpart-websocket-fixed.conf to enable SSL and adjust WebSocket proxy settings.
- Streamlined fallback logic in frontend store to ensure direct connection to the daemon on port 4551.
- Enhanced logging for better debugging and monitoring of daemon connections.
2026-01-14 13:02:38 +01:00

545 lines
20 KiB
JavaScript

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;