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 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-05-28 01:31:46 +02:00
parent 916123f3b4
commit 239bf0dad6
2 changed files with 61 additions and 5 deletions

View File

@ -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:

View File

@ -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;