From 2ee3432bbc899ec0e3b6dd22d6c2dfd02334f4ed Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Fri, 29 May 2026 15:17:02 +0200 Subject: [PATCH] Persist engine container names and card URLs to server settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously: container names and dynamic card URL overrides only written to localStorage — lost when accessing from a different browser or after clearing browser data. Now: - engine_container_names added as persisted settings key (dict) - All container name inputs tagged data-cn-key for loadSettings() lookup - loadSettings() restores container names + dynamic URLs from server - settings.js exposes _saveEngineContainerNames() and _saveEngineLocalUrls() globally so engines.js / ai-backends.js can call them on every input event - Static cards (ai-backends.js initStaticDockerManagement): reads saved name from engine_container_names first, localStorage fallback - Dynamic Docker stack cards (engines.js): same priority for both URL and container name; both write to server on change Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 8 ++++++++ core/config.py | 6 +++++- static/js/ai-backends.js | 13 ++++++++----- static/js/engines.js | 24 ++++++++++++++++-------- static/js/settings.js | 36 ++++++++++++++++++++++++++++++++++++ 5 files changed, 73 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9b8bc5..a55a19b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,14 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi ### Fixed +- **Engine settings lost after container recreate** — container names and + dynamic Docker card URL overrides were stored only in `localStorage`. + Added `engine_container_names` as a persisted server setting; all + container name inputs tagged with `data-cn-key` so `loadSettings()` + can restore them; dynamic card URL overrides now saved under + `engine_local_urls` with a `dc-` prefix. On first page load the server + wins over `localStorage`; changes write to both immediately. + - **`ImportError: cannot import name '_AUDIO_EXTS' from 'core.audio'`** — `routes/stt.py` had a dead alias `from core.audio import _to_wav_16k, _AUDIO_EXTS as _VOICE_AUDIO_EXTS`; `_AUDIO_EXTS` lives in `core.voice`, not `core.audio`. Removed the diff --git a/core/config.py b/core/config.py index 2023669..4d4c40a 100644 --- a/core/config.py +++ b/core/config.py @@ -37,7 +37,8 @@ _SETTINGS_KEYS = { "client_voice_bindings", "llm_url", # Browser-persistent UI state - "engine_local_urls", "custom_engine_cards", "refine_llm_url", "conv_llm_url", + "engine_local_urls", "engine_container_names", "custom_engine_cards", + "refine_llm_url", "conv_llm_url", } # ── TTS stability defaults ──────────────────────────────────────────────────── @@ -203,6 +204,8 @@ def _normalize_settings(s: dict) -> dict: s["tts_stream_mode"] = "auto" if not isinstance(s.get("engine_local_urls"), dict): s["engine_local_urls"] = {} + if not isinstance(s.get("engine_container_names"), dict): + s["engine_container_names"] = {} if not isinstance(s.get("custom_engine_cards"), list): s["custom_engine_cards"] = [] return s @@ -258,6 +261,7 @@ def _load_settings() -> dict: "client_voice_bindings": {}, "llm_url": "http://localhost:11434/v1", "engine_local_urls": {}, + "engine_container_names": {}, "custom_engine_cards": [], "refine_llm_url": "", "conv_llm_url": "", diff --git a/static/js/ai-backends.js b/static/js/ai-backends.js index 557836b..7fc77e2 100644 --- a/static/js/ai-backends.js +++ b/static/js/ai-backends.js @@ -331,18 +331,20 @@ initLlmSnippets(document); if (!urlInp) return; card.dataset.dockerInit = '1'; - const key = urlInp.dataset.llmLocalKey; + const key = urlInp.dataset.llmLocalKey; + const lsKey = 'llm-docker-name-' + key; // ── 1. Container name row (always injected, right after URL row) ────── const urlRow = urlInp.closest('.llm-local-url'); - const nameKey = 'llm-docker-name-' + key; + const savedName = (_appSettings?.engine_container_names?.[key]) + || localStorage.getItem(lsKey) || ''; const nameRow = document.createElement('div'); nameRow.className = 'llm-local-url'; nameRow.innerHTML = `` + - ``; + ``; const nameInp = nameRow.querySelector('input'); - nameInp.value = localStorage.getItem(nameKey) || ''; + nameInp.value = savedName; if (urlRow) urlRow.after(nameRow); // ── 2. Unified controls row ─────────────────────────────────────────── @@ -462,7 +464,8 @@ initLlmSnippets(document); }); } nameInp.addEventListener('input', () => { - localStorage.setItem(nameKey, nameInp.value); + localStorage.setItem(lsKey, nameInp.value); + if (window._saveEngineContainerNames) window._saveEngineContainerNames(); renderDockerBtns(); }); renderDockerBtns(); diff --git a/static/js/engines.js b/static/js/engines.js index ef742c3..ab1f611 100644 --- a/static/js/engines.js +++ b/static/js/engines.js @@ -378,14 +378,17 @@ function renderLocalContainers(containers) { `; // Container name row — always shown so users can pre-configure even absent containers - const containerNameValue = isCustom ? (c.containerName || '') : (installed ? c.name : ''); + const savedContainerName = isCustom + ? (c.containerName || '') + : ((_appSettings?.engine_container_names?.[c.name]) || (installed ? c.name : '')); const containerNameRowHtml = `
+ value="${escHtml(savedContainerName)}" + data-dc-initial-running="${running}" + data-cn-key="${n}">
`; // Use-as button — right-aligned, clearly labelled as the activation action @@ -506,12 +509,16 @@ function renderLocalContainers(containers) { } } - // URL input — persist to localStorage, restore connected state on redraw + // URL input — server settings first, localStorage fallback; write to both on change grid.querySelectorAll('.dc-url-inp[data-dc-url-key]').forEach(inp => { - const key = inp.dataset.dcUrlKey; - const saved = localStorage.getItem('dc-url-' + key); - if (saved) inp.value = saved; - inp.addEventListener('input', () => localStorage.setItem('dc-url-' + key, inp.value)); + const key = inp.dataset.dcUrlKey; + const fromSrv = _appSettings?.engine_local_urls?.['dc-' + key]; + const fromLS = localStorage.getItem('dc-url-' + key); + if (fromSrv || fromLS) inp.value = fromSrv || fromLS; + inp.addEventListener('input', () => { + localStorage.setItem('dc-url-' + key, inp.value); + if (window._saveEngineLocalUrls) window._saveEngineLocalUrls(); + }); if (localStorage.getItem('dc-con-' + key) === '1') { const card = inp.closest('.llm-local-card'); const btn = card?.querySelector(`.dc-connect-btn[data-dc-url-key="${CSS.escape(key)}"]`); @@ -648,6 +655,7 @@ function renderLocalContainers(containers) { inp.addEventListener('input', () => { const name = inp.value.trim(); + if (window._saveEngineContainerNames) window._saveEngineContainerNames(); renderDockerBtns(name); // no initial state after user edits → show all 3 buttons if (customId) { const updated = loadCustomEngineCards().map(c => diff --git a/static/js/settings.js b/static/js/settings.js index ad33d54..9820f2b 100644 --- a/static/js/settings.js +++ b/static/js/settings.js @@ -177,12 +177,34 @@ function _saveEngineLocalUrls() { clearTimeout(_engineUrlSaveTimer); _engineUrlSaveTimer = setTimeout(() => { const urls = {}; + // Static cards (data-llm-local-key) document.querySelectorAll('[data-llm-local-key]').forEach(i => { if (i.value) urls[i.dataset.llmLocalKey] = i.value; }); + // Dynamic Docker stack card URL overrides (data-dc-url-key) + document.querySelectorAll('.dc-url-inp[data-dc-url-key]').forEach(i => { + const v = i.value.trim(); + if (v && v !== i.placeholder) urls['dc-' + i.dataset.dcUrlKey] = v; + }); _patchSettings({ engine_local_urls: urls }); if (_appSettings) _appSettings.engine_local_urls = urls; }, 800); } +let _containerNameSaveTimer = null; +function _saveEngineContainerNames() { + clearTimeout(_containerNameSaveTimer); + _containerNameSaveTimer = setTimeout(() => { + const names = {}; + document.querySelectorAll('[data-cn-key]').forEach(inp => { + const v = inp.value.trim(); + if (v) names[inp.dataset.cnKey] = v; + }); + _patchSettings({ engine_container_names: names }); + if (_appSettings) _appSettings.engine_container_names = names; + }, 800); +} +window._saveEngineLocalUrls = _saveEngineLocalUrls; +window._saveEngineContainerNames = _saveEngineContainerNames; + async function loadSettings() { const s = await fetch('/api/settings').then(r => r.json()); $('s-whisper-url').value = s.whisper_url || ''; @@ -203,6 +225,20 @@ async function loadSettings() { const v = savedEngineUrls[inp.dataset.llmLocalKey]; if (v) inp.value = v; }); + document.querySelectorAll('.dc-url-inp[data-dc-url-key]').forEach(inp => { + const v = savedEngineUrls['dc-' + inp.dataset.dcUrlKey]; + if (v) inp.value = v; + }); + + // Restore container names from server — uses data-cn-key tags set by engines.js / ai-backends.js + const savedContainerNames = s.engine_container_names || {}; + document.querySelectorAll('[data-cn-key]').forEach(inp => { + const name = savedContainerNames[inp.dataset.cnKey]; + if (name && !inp.value) { + inp.value = name; + inp.dispatchEvent(new Event('input', { bubbles: true })); + } + }); // Restore LLM URLs for refinement and conversation panels const refineInp = $('refine-llm-url');