From 7c5d194376807459b046140fb2d2fd65010af78f Mon Sep 17 00:00:00 2001 From: Georg Tomitsch Date: Tue, 19 May 2026 17:08:48 +0200 Subject: [PATCH] Stabilize TTS voice reload and reconnect logging --- public/index.html | 20 +------- public/js/logger.js | 85 +++++++++++++++++++++++++++++++ public/js/socket-client-module.js | 61 ++++++++++++++++++++-- public/js/tts-factory-module.js | 3 +- public/locales/de_DE.json | 3 +- public/locales/en_US.json | 3 +- 6 files changed, 148 insertions(+), 27 deletions(-) create mode 100644 public/js/logger.js diff --git a/public/index.html b/public/index.html index 9961aa8..6c63a81 100644 --- a/public/index.html +++ b/public/index.html @@ -245,6 +245,7 @@ border: 1px none rgba(255,255,255,0); } + @@ -279,25 +280,6 @@ console.log(message); }; - diff --git a/public/js/logger.js b/public/js/logger.js new file mode 100644 index 0000000..f318ea3 --- /dev/null +++ b/public/js/logger.js @@ -0,0 +1,85 @@ +(function () { + const originalConsole = { + debug: console.debug.bind(console), + log: console.log.bind(console), + info: console.info.bind(console), + warn: console.warn.bind(console), + error: console.error.bind(console) + }; + + const levels = { + silent: 0, + error: 1, + warn: 2, + info: 3, + debug: 4 + }; + + function readLevel() { + const params = new URLSearchParams(window.location.search); + const queryLevel = params.get('log') || params.get('debug'); + if (queryLevel === '1' || queryLevel === 'true') return 'debug'; + if (queryLevel && levels[queryLevel]) return queryLevel; + + const savedLevel = localStorage.getItem('ai-if-log-level'); + if (savedLevel && Object.prototype.hasOwnProperty.call(levels, savedLevel)) { + return savedLevel; + } + + return 'warn'; + } + + const logger = { + level: readLevel(), + levels, + originalConsole, + + setLevel(level) { + if (!Object.prototype.hasOwnProperty.call(levels, level)) { + originalConsole.warn(`Unknown log level "${level}". Use silent, error, warn, info, or debug.`); + return; + } + this.level = level; + localStorage.setItem('ai-if-log-level', level); + originalConsole.info(`AI IF log level set to ${level}`); + }, + + shouldPrint(level) { + return levels[this.level] >= levels[level]; + }, + + write(level, args) { + if (!this.shouldPrint(level)) return; + const writer = originalConsole[level] || originalConsole.log; + writer(...args); + }, + + debug(...args) { + this.write('debug', args); + }, + + log(...args) { + this.write('debug', args); + }, + + info(...args) { + this.write('info', args); + }, + + warn(...args) { + this.write('warn', args); + }, + + error(...args) { + this.write('error', args); + } + }; + + console.debug = (...args) => logger.debug(...args); + console.log = (...args) => logger.log(...args); + console.info = (...args) => logger.info(...args); + console.warn = (...args) => logger.warn(...args); + console.error = (...args) => logger.error(...args); + + window.AppLogger = logger; +})(); diff --git a/public/js/socket-client-module.js b/public/js/socket-client-module.js index 0207873..f3a32e3 100644 --- a/public/js/socket-client-module.js +++ b/public/js/socket-client-module.js @@ -19,9 +19,11 @@ class SocketClientModule extends BaseModule { this.storyHistory = null; this.isConnected = false; this.reconnectAttempts = 0; - this.maxReconnectAttempts = Infinity; - this.reconnectDelay = 2000; - this.maxReconnectDelay = 30000; + this.maxReconnectAttempts = 12; + this.reconnectDelay = 5000; + this.maxReconnectDelay = 5000; + this.reconnectTimer = null; + this.reconnectAlerted = false; this.url = null; this.eventListeners = {}; this.defaultHost = 'localhost:3000'; @@ -77,6 +79,8 @@ class SocketClientModule extends BaseModule { 'resolveAssetUrl', 'looksLikeAssetPath', 'attemptReconnect', + 'stopReconnectLoop', + 'notifyReconnectFailed', 'getConnectionStatus', 'loadSocketIO' ]); @@ -152,6 +156,12 @@ class SocketClientModule extends BaseModule { try { console.log(`Socket Client: Connecting to ${socketUrl}`); + if (this.socket) { + this.socket.removeAllListeners(); + this.socket.close(); + this.socket = null; + } + // Create Socket.IO connection (will automatically use /socket.io endpoint) this.socket = window.io(socketUrl, { reconnection: false, // We handle reconnection ourselves @@ -162,6 +172,8 @@ class SocketClientModule extends BaseModule { console.log('Socket Client: Connected to server with ID:', this.socket.id); this.isConnected = true; this.reconnectAttempts = 0; + this.stopReconnectLoop(); + this.reconnectAlerted = false; this.emitEvent('connect'); resolve(true); }); @@ -177,6 +189,9 @@ class SocketClientModule extends BaseModule { this.socket.on('connect_error', (error) => { console.error('Socket Client: Connection error:', error); this.emitEvent('connect_error', error); + if (!this.isConnected) { + this.attemptReconnect(); + } resolve(false); }); @@ -659,7 +674,11 @@ class SocketClientModule extends BaseModule { */ attemptReconnect() { if (this.reconnectAttempts >= this.maxReconnectAttempts) { - console.error('Socket Client: Max reconnect attempts reached'); + this.notifyReconnectFailed(); + return; + } + + if (this.reconnectTimer) { return; } @@ -667,13 +686,45 @@ class SocketClientModule extends BaseModule { const delay = Math.min(this.maxReconnectDelay, this.reconnectDelay * this.reconnectAttempts); console.log(`Socket Client: Attempting to reconnect in ${delay}ms (attempt ${this.reconnectAttempts})`); + document.dispatchEvent(new CustomEvent('story:process-state', { + detail: { state: 'waiting-generating', reason: 'socket-reconnecting', attempt: this.reconnectAttempts } + })); - setTimeout(() => { + this.reconnectTimer = setTimeout(() => { + this.reconnectTimer = null; if (!this.isConnected) { this.connect(); } }, delay); } + + stopReconnectLoop() { + if (this.reconnectTimer) { + clearTimeout(this.reconnectTimer); + this.reconnectTimer = null; + } + } + + notifyReconnectFailed() { + if (this.reconnectAlerted) return; + this.reconnectAlerted = true; + this.stopReconnectLoop(); + const message = this.translate( + 'popup.serverUnavailable', + 'The game server is currently unavailable. The client tried to reconnect for one minute. Please reload the page after the server is running again.' + ); + console.error('Socket Client: Reconnect failed after one minute'); + document.dispatchEvent(new CustomEvent('story:tag', { + detail: { + key: 'error', + value: message, + source: 'socket-reconnect' + } + })); + document.dispatchEvent(new CustomEvent('story:process-state', { + detail: { state: 'ready', reason: 'socket-reconnect-failed' } + })); + } /** * Disconnect from the server diff --git a/public/js/tts-factory-module.js b/public/js/tts-factory-module.js index eda6a1e..a3147c7 100644 --- a/public/js/tts-factory-module.js +++ b/public/js/tts-factory-module.js @@ -1199,7 +1199,8 @@ class TTSFactoryModule extends BaseModule { } else if (handler && typeof handler.configure === 'function') { handler.configure(voiceOptions); } - if (voiceOptions.language && !voiceOptions.voice && handler && typeof handler.selectVoiceForLocale === 'function') { + const handlerHasVoice = Boolean(this.getEffectiveVoiceId(handler)); + if (voiceOptions.language && !voiceOptions.voice && !handlerHasVoice && handler && typeof handler.selectVoiceForLocale === 'function') { handler.selectVoiceForLocale(voiceOptions.language); } } diff --git a/public/locales/de_DE.json b/public/locales/de_DE.json index ea6fe7f..eacb2eb 100644 --- a/public/locales/de_DE.json +++ b/public/locales/de_DE.json @@ -64,5 +64,6 @@ "popup.defaultError": "Das Spiel wurde wegen eines nicht behebbaren Fehlers beendet.", "popup.defaultAchievement": "Errungenschaft freigeschaltet.", "popup.defaultAlert": "Hinweis", - "popup.commandTimeout": "Der Spielserver hat nicht rechtzeitig geantwortet. Du kannst es noch einmal versuchen." + "popup.commandTimeout": "Der Spielserver hat nicht rechtzeitig geantwortet. Du kannst es noch einmal versuchen.", + "popup.serverUnavailable": "Der Spielserver ist im Moment nicht erreichbar. Der Client hat eine Minute lang versucht, die Verbindung wiederherzustellen. Bitte lade die Seite neu, sobald der Server wieder läuft." } diff --git a/public/locales/en_US.json b/public/locales/en_US.json index efe8bd1..802234b 100644 --- a/public/locales/en_US.json +++ b/public/locales/en_US.json @@ -64,5 +64,6 @@ "popup.defaultError": "The game ended because of an unrecoverable error.", "popup.defaultAchievement": "Achievement unlocked.", "popup.defaultAlert": "Hint", - "popup.commandTimeout": "The game server did not answer in time. You can try again." + "popup.commandTimeout": "The game server did not answer in time. You can try again.", + "popup.serverUnavailable": "The game server is currently unavailable. The client tried to reconnect for one minute. Please reload the page after the server is running again." }