Persist engine container names and card URLs to server settings
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 <noreply@anthropic.com>
This commit is contained in:
parent
686fd61865
commit
2ee3432bbc
@ -9,6 +9,14 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
|||||||
|
|
||||||
### Fixed
|
### 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'`** —
|
- **`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`;
|
`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
|
`_AUDIO_EXTS` lives in `core.voice`, not `core.audio`. Removed the
|
||||||
|
|||||||
@ -37,7 +37,8 @@ _SETTINGS_KEYS = {
|
|||||||
"client_voice_bindings",
|
"client_voice_bindings",
|
||||||
"llm_url",
|
"llm_url",
|
||||||
# Browser-persistent UI state
|
# 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 ────────────────────────────────────────────────────
|
# ── TTS stability defaults ────────────────────────────────────────────────────
|
||||||
@ -203,6 +204,8 @@ def _normalize_settings(s: dict) -> dict:
|
|||||||
s["tts_stream_mode"] = "auto"
|
s["tts_stream_mode"] = "auto"
|
||||||
if not isinstance(s.get("engine_local_urls"), dict):
|
if not isinstance(s.get("engine_local_urls"), dict):
|
||||||
s["engine_local_urls"] = {}
|
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):
|
if not isinstance(s.get("custom_engine_cards"), list):
|
||||||
s["custom_engine_cards"] = []
|
s["custom_engine_cards"] = []
|
||||||
return s
|
return s
|
||||||
@ -258,6 +261,7 @@ def _load_settings() -> dict:
|
|||||||
"client_voice_bindings": {},
|
"client_voice_bindings": {},
|
||||||
"llm_url": "http://localhost:11434/v1",
|
"llm_url": "http://localhost:11434/v1",
|
||||||
"engine_local_urls": {},
|
"engine_local_urls": {},
|
||||||
|
"engine_container_names": {},
|
||||||
"custom_engine_cards": [],
|
"custom_engine_cards": [],
|
||||||
"refine_llm_url": "",
|
"refine_llm_url": "",
|
||||||
"conv_llm_url": "",
|
"conv_llm_url": "",
|
||||||
|
|||||||
@ -332,17 +332,19 @@ initLlmSnippets(document);
|
|||||||
card.dataset.dockerInit = '1';
|
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) ──────
|
// ── 1. Container name row (always injected, right after URL row) ──────
|
||||||
const urlRow = urlInp.closest('.llm-local-url');
|
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');
|
const nameRow = document.createElement('div');
|
||||||
nameRow.className = 'llm-local-url';
|
nameRow.className = 'llm-local-url';
|
||||||
nameRow.innerHTML =
|
nameRow.innerHTML =
|
||||||
`<span class="llm-local-url-label" title="Docker container name — enables Stop / Start / Restart"><span class="mdi mdi-docker"></span></span>` +
|
`<span class="llm-local-url-label" title="Docker container name — enables Stop / Start / Restart"><span class="mdi mdi-docker"></span></span>` +
|
||||||
`<input class="llm-local-url-inp" type="text" placeholder="container name (optional)" spellcheck="false">`;
|
`<input class="llm-local-url-inp" type="text" placeholder="container name (optional)" spellcheck="false" data-cn-key="${key}">`;
|
||||||
const nameInp = nameRow.querySelector('input');
|
const nameInp = nameRow.querySelector('input');
|
||||||
nameInp.value = localStorage.getItem(nameKey) || '';
|
nameInp.value = savedName;
|
||||||
if (urlRow) urlRow.after(nameRow);
|
if (urlRow) urlRow.after(nameRow);
|
||||||
|
|
||||||
// ── 2. Unified controls row ───────────────────────────────────────────
|
// ── 2. Unified controls row ───────────────────────────────────────────
|
||||||
@ -462,7 +464,8 @@ initLlmSnippets(document);
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
nameInp.addEventListener('input', () => {
|
nameInp.addEventListener('input', () => {
|
||||||
localStorage.setItem(nameKey, nameInp.value);
|
localStorage.setItem(lsKey, nameInp.value);
|
||||||
|
if (window._saveEngineContainerNames) window._saveEngineContainerNames();
|
||||||
renderDockerBtns();
|
renderDockerBtns();
|
||||||
});
|
});
|
||||||
renderDockerBtns();
|
renderDockerBtns();
|
||||||
|
|||||||
@ -378,14 +378,17 @@ function renderLocalContainers(containers) {
|
|||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
// Container name row — always shown so users can pre-configure even absent 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 = `
|
const containerNameRowHtml = `
|
||||||
<div class="llm-local-url">
|
<div class="llm-local-url">
|
||||||
<span class="llm-local-url-label" title="Docker container name — enables Stop / Start / Restart"><span class="mdi mdi-docker"></span></span>
|
<span class="llm-local-url-label" title="Docker container name — enables Stop / Start / Restart"><span class="mdi mdi-docker"></span></span>
|
||||||
<input class="llm-local-url-inp dc-container-name-inp" type="text"
|
<input class="llm-local-url-inp dc-container-name-inp" type="text"
|
||||||
placeholder="container name (optional)" spellcheck="false"
|
placeholder="container name (optional)" spellcheck="false"
|
||||||
value="${escHtml(containerNameValue)}"
|
value="${escHtml(savedContainerName)}"
|
||||||
data-dc-initial-running="${running}">
|
data-dc-initial-running="${running}"
|
||||||
|
data-cn-key="${n}">
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
// Use-as button — right-aligned, clearly labelled as the activation action
|
// 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 => {
|
grid.querySelectorAll('.dc-url-inp[data-dc-url-key]').forEach(inp => {
|
||||||
const key = inp.dataset.dcUrlKey;
|
const key = inp.dataset.dcUrlKey;
|
||||||
const saved = localStorage.getItem('dc-url-' + key);
|
const fromSrv = _appSettings?.engine_local_urls?.['dc-' + key];
|
||||||
if (saved) inp.value = saved;
|
const fromLS = localStorage.getItem('dc-url-' + key);
|
||||||
inp.addEventListener('input', () => localStorage.setItem('dc-url-' + key, inp.value));
|
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') {
|
if (localStorage.getItem('dc-con-' + key) === '1') {
|
||||||
const card = inp.closest('.llm-local-card');
|
const card = inp.closest('.llm-local-card');
|
||||||
const btn = card?.querySelector(`.dc-connect-btn[data-dc-url-key="${CSS.escape(key)}"]`);
|
const btn = card?.querySelector(`.dc-connect-btn[data-dc-url-key="${CSS.escape(key)}"]`);
|
||||||
@ -648,6 +655,7 @@ function renderLocalContainers(containers) {
|
|||||||
|
|
||||||
inp.addEventListener('input', () => {
|
inp.addEventListener('input', () => {
|
||||||
const name = inp.value.trim();
|
const name = inp.value.trim();
|
||||||
|
if (window._saveEngineContainerNames) window._saveEngineContainerNames();
|
||||||
renderDockerBtns(name); // no initial state after user edits → show all 3 buttons
|
renderDockerBtns(name); // no initial state after user edits → show all 3 buttons
|
||||||
if (customId) {
|
if (customId) {
|
||||||
const updated = loadCustomEngineCards().map(c =>
|
const updated = loadCustomEngineCards().map(c =>
|
||||||
|
|||||||
@ -177,12 +177,34 @@ function _saveEngineLocalUrls() {
|
|||||||
clearTimeout(_engineUrlSaveTimer);
|
clearTimeout(_engineUrlSaveTimer);
|
||||||
_engineUrlSaveTimer = setTimeout(() => {
|
_engineUrlSaveTimer = setTimeout(() => {
|
||||||
const urls = {};
|
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; });
|
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 });
|
_patchSettings({ engine_local_urls: urls });
|
||||||
if (_appSettings) _appSettings.engine_local_urls = urls;
|
if (_appSettings) _appSettings.engine_local_urls = urls;
|
||||||
}, 800);
|
}, 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() {
|
async function loadSettings() {
|
||||||
const s = await fetch('/api/settings').then(r => r.json());
|
const s = await fetch('/api/settings').then(r => r.json());
|
||||||
$('s-whisper-url').value = s.whisper_url || '';
|
$('s-whisper-url').value = s.whisper_url || '';
|
||||||
@ -203,6 +225,20 @@ async function loadSettings() {
|
|||||||
const v = savedEngineUrls[inp.dataset.llmLocalKey];
|
const v = savedEngineUrls[inp.dataset.llmLocalKey];
|
||||||
if (v) inp.value = v;
|
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
|
// Restore LLM URLs for refinement and conversation panels
|
||||||
const refineInp = $('refine-llm-url');
|
const refineInp = $('refine-llm-url');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user