From 239bf0dad6a6e865620fd613ed88d3d2428bb838 Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Thu, 28 May 2026 01:31:46 +0200 Subject: [PATCH] Persist engine URLs and custom cards server-side (survive browser restarts) Engine URL inputs (Ollama, vLLM, faster-whisper, etc.), custom engine cards, and the refinement/conversation LLM URLs were stored only in localStorage and lost on browser data clear. All four are now synced to settings.json via _patchSettings() with localStorage as fast initial fallback. Co-Authored-By: Claude Sonnet 4.6 --- server.py | 10 +++++++++ static/app.js | 56 ++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/server.py b/server.py index 890010f..2c575e1 100644 --- a/server.py +++ b/server.py @@ -326,6 +326,8 @@ _SETTINGS_KEYS = { "captures_default_voice", "client_voice_bindings", "llm_url", + # Browser-persistent UI state + "engine_local_urls", "custom_engine_cards", "refine_llm_url", "conv_llm_url", } @@ -488,6 +490,10 @@ def _normalize_settings(s: dict) -> dict: s["output_dir"] = str(scan_dir / "active_voices") if s.get("tts_stream_mode") not in {"auto", "streaming", "buffered"}: s["tts_stream_mode"] = "auto" + if not isinstance(s.get("engine_local_urls"), dict): + s["engine_local_urls"] = {} + if not isinstance(s.get("custom_engine_cards"), list): + s["custom_engine_cards"] = [] return s @@ -532,6 +538,10 @@ def _load_settings() -> dict: "captures_default_voice": "", "client_voice_bindings": {}, "llm_url": "http://localhost:11434/v1", + "engine_local_urls": {}, + "custom_engine_cards": [], + "refine_llm_url": "", + "conv_llm_url": "", } if CONFIG_FILE.exists(): try: diff --git a/static/app.js b/static/app.js index ca1ba9d..b6c4aa8 100644 --- a/static/app.js +++ b/static/app.js @@ -1955,6 +1955,23 @@ async function refreshTtsBackendAvailability(selected = '') { return _ttsBackends; } +async function _patchSettings(patch) { + try { + await fetch('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(patch) }); + } catch (e) { console.warn('[settings] patch failed', e); } +} + +let _engineUrlSaveTimer = null; +function _saveEngineLocalUrls() { + clearTimeout(_engineUrlSaveTimer); + _engineUrlSaveTimer = setTimeout(() => { + const urls = {}; + document.querySelectorAll('[data-llm-local-key]').forEach(i => { if (i.value) urls[i.dataset.llmLocalKey] = i.value; }); + _patchSettings({ engine_local_urls: urls }); + if (_appSettings) _appSettings.engine_local_urls = urls; + }, 800); +} + async function loadSettings() { const s = await fetch('/api/settings').then(r => r.json()); $('s-whisper-url').value = s.whisper_url || ''; @@ -1968,6 +1985,19 @@ async function loadSettings() { $('s-xtts-url').value = s.xtts_url || ''; const llmUrlEl = $('s-llm-url'); if (llmUrlEl) llmUrlEl.value = s.llm_url || ''; _appSettings = s; + + // Restore engine URL inputs from server (overrides localStorage fallback) + const savedEngineUrls = s.engine_local_urls || {}; + document.querySelectorAll('[data-llm-local-key]').forEach(inp => { + const v = savedEngineUrls[inp.dataset.llmLocalKey]; + if (v) inp.value = v; + }); + + // Restore LLM URLs for refinement and conversation panels + const refineInp = $('refine-llm-url'); + if (refineInp && s.refine_llm_url) refineInp.value = s.refine_llm_url; + const convInp = $('conv-llm-url'); + if (convInp && s.conv_llm_url) convInp.value = s.conv_llm_url; $('s-tts-stream-url').value = s.tts_stream_url || ''; $('s-customvoice-url').value = s.customvoice_url || 'http://host.docker.internal:8022'; $('s-nvidia-router-url').value = s.nvidia_router_url || 'http://host.docker.internal:8090'; @@ -6842,11 +6872,15 @@ loadLocalContainers(); // ── Custom engine card storage helpers ──────────────────────────────────── function loadCustomEngineCards() { + if (_appSettings && Array.isArray(_appSettings.custom_engine_cards) && _appSettings.custom_engine_cards.length) + return _appSettings.custom_engine_cards; try { return JSON.parse(localStorage.getItem('engines-custom-cards') || '[]'); } catch (e) { return []; } } function saveCustomEngineCards(cards) { localStorage.setItem('engines-custom-cards', JSON.stringify(cards)); + if (_appSettings) _appSettings.custom_engine_cards = cards; + _patchSettings({ custom_engine_cards: cards }); } // Settings key per docker container name; role fallback for custom cards @@ -7303,9 +7337,9 @@ document.querySelectorAll('.dc-refresh-btn').forEach(b => b.addEventListener('cl if (!card) return; const btn = card.querySelector('.llm-local-ping'); - const savedUrl = localStorage.getItem('llm-local-url-' + key); + const savedUrl = (_appSettings && _appSettings.engine_local_urls && _appSettings.engine_local_urls[key]) || localStorage.getItem('llm-local-url-' + key); if (savedUrl) inp.value = savedUrl; - inp.addEventListener('input', () => { localStorage.setItem('llm-local-url-' + key, inp.value); }); + inp.addEventListener('input', () => { localStorage.setItem('llm-local-url-' + key, inp.value); _saveEngineLocalUrls(); }); if (localStorage.getItem('llm-local-con-' + key) === '1') applyCardState(card, key, true, false); @@ -7979,10 +8013,15 @@ let _refineOriginal = null; (function initLlmRefinement() { const inp = $('refine-llm-url'); if (!inp) return; - const saved = localStorage.getItem('refine-llm-url'); + // loadSettings() will overwrite with the server value; localStorage is the fast initial fallback + const saved = (_appSettings && _appSettings.refine_llm_url) || localStorage.getItem('refine-llm-url'); if (saved) inp.value = saved; - else inp.value = localStorage.getItem('llm-local-url-ollama') || 'http://localhost:11434/v1'; - inp.addEventListener('input', () => localStorage.setItem('refine-llm-url', inp.value)); + else inp.value = (_appSettings && _appSettings.engine_local_urls && _appSettings.engine_local_urls['ollama']) || localStorage.getItem('llm-local-url-ollama') || 'http://localhost:11434/v1'; + inp.addEventListener('input', () => { + localStorage.setItem('refine-llm-url', inp.value); + _patchSettings({ refine_llm_url: inp.value }); + if (_appSettings) _appSettings.refine_llm_url = inp.value; + }); })(); function updateRefineButtonState() { @@ -8149,6 +8188,13 @@ $('s-import-voices-file')?.addEventListener('change', async function () { const turnHistory = $('conv-turn-history'); if (!chatWindow || !micBtn) return; + // Restore conv LLM URL from server settings + if (llmUrlInp && _appSettings && _appSettings.conv_llm_url) llmUrlInp.value = _appSettings.conv_llm_url; + if (llmUrlInp) llmUrlInp.addEventListener('input', () => { + _patchSettings({ conv_llm_url: llmUrlInp.value }); + if (_appSettings) _appSettings.conv_llm_url = llmUrlInp.value; + }); + let mediaRecorder = null; let recChunks = []; let recTimerInterval = null;