fix(ui): surface silently swallowed save and fetch errors

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-10-06 00:44:31 +02:00
parent 2479617964
commit 60eb108360
16 changed files with 32 additions and 30 deletions

View File

@ -12,6 +12,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
### Fixed
- Async routes no longer block the event loop on synchronous HTTP: seed proxy/samples, text refine/persona rewrite, voice matching (its retry back-off is now `asyncio.sleep`), character deep analysis, character image-from-URL, LLM/engine model listing, source audio import/proxy and ElevenLabs shared voices now run `requests` through `asyncio.to_thread` (same timeouts and responses).
- Silently swallowed errors now surface: failed saves of the audiobook prompt default, generated character sheets and character colours show an error toast, and ~20 other hidden failures (rehearsal/cast write-back, reader progress and audio fetches, seed-finder DB writes, settings/model fetches, export server save) are logged with `logErr` instead of being dropped.
### Changed
- Conversation Playground: the Language Model base URL is now picked from a dropdown of the language models configured under Engines (Docker LLM containers incl. saved URL overrides, and custom LLM cards; running/stopped marked), with a "Custom…" option that reveals the free-text URL. Selecting an entry saves `conv_llm_url` and fetches the models.
- Playwright specs now import `test` from `tests/_guard.js`, whose auto fixture aborts and fails any unmocked non-GET request to `/api/**` so a spec can no longer modify live settings. See `architecture/qa.md`.

File diff suppressed because one or more lines are too long

View File

@ -434,7 +434,7 @@
url = s.llm_url || '';
if (!_appSettings) _appSettings = s;
else { _appSettings.llm_url = s.llm_url || _appSettings.llm_url; _appSettings.llm_model = s.llm_model || _appSettings.llm_model; }
} catch (_) {}
} catch (_) { logErr('llm-url-settings', _); }
}
// wait a tick for docker cards to render so options are available
@ -564,7 +564,7 @@
_appSettings.vision_ocr_model = s.vision_ocr_model || _appSettings.vision_ocr_model;
_appSettings.vision_ocr_enabled = s.vision_ocr_enabled;
}
} catch (_) {}
} catch (_) { logErr('ocr-settings', _); }
}
if (enabledCb) enabledCb.checked = enabled !== false;
setTimeout(() => {
@ -593,7 +593,7 @@
`<option value="${escHtml(b.id)}"${!b.available ? ' disabled' : ''}>${b.available ? '✓' : '✗'} ${escHtml(b.label)}</option>`
).join('');
if (prev && sel.querySelector(`option[value="${CSS.escape(prev)}"]`)) sel.value = prev;
} catch(_) {}
} catch (_) { logErr('backend-list', _); }
}
refreshSttTestBackends();

View File

@ -652,7 +652,7 @@ STRIKTE FORMAT- UND TEXTREGELN:
// Also save as global default for next time
if (typeof _appSettings !== 'undefined') _appSettings.audiobook_prompt = val;
try { await fetch('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ audiobook_prompt: val }) }); } catch (e) {}
try { await fetch('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ audiobook_prompt: val }) }); } catch (e) { logErr('audiobook-prompt-save', e); toast('Could not save the prompt as the default', 'error'); }
});
// Sync the LLM engine dropdown with global options
@ -1163,7 +1163,7 @@ STRIKTE FORMAT- UND TEXTREGELN:
: (typeof clGetAll === 'function' ? await clGetAll() : []);
for (const r of all || []) registerCharacterRecord(r);
rec = recordForName(name);
} catch (_) {}
} catch (_) { logErr('character-record-load', _); }
const detail = document.createElement('div');
detail.className = 'ab-char-detail-panel';
@ -1256,7 +1256,7 @@ STRIKTE FORMAT- UND TEXTREGELN:
_abRefreshCharacterColors(rec.name);
clearTimeout(_audiobook._colorSaveTimer);
_audiobook._colorSaveTimer = setTimeout(async () => {
try { if (typeof clPut === 'function') await clPut(rec); } catch (_) {}
try { if (typeof clPut === 'function') await clPut(rec); } catch (_) { logErr('character-colour-save', _); toast('Could not save the character colour', 'error'); }
}, 400);
});
detail.querySelector('.ab-cd-pick')?.addEventListener('click', () => {

View File

@ -507,7 +507,7 @@ async function audiobookOpenCastView() {
const bookId = _abBookId();
let chosen = _localDraft, source = 'local', serverDraft = null;
if (bookId) {
try { serverDraft = await _abLoadDraftServer(bookId); } catch (_) {}
try { serverDraft = await _abLoadDraftServer(bookId); } catch (_) { logErr('draft-server-load', _); }
if (serverDraft && (serverDraft.savedAt || 0) > (_localDraft.savedAt || 0)) {
chosen = serverDraft; source = 'server';
try { localStorage.setItem(_abDraftKey(bookId), JSON.stringify(serverDraft)); } catch (_) {}

View File

@ -132,7 +132,7 @@ async function audiobookOpenInRehearser(script, title, dialogueEmotions) {
// Script Rehearser → Bibliothek (Library) and reopen anytime to edit & synthesise.
let saved = false;
if (typeof saveToLibrary === 'function') {
try { rehState.savedId = null; await saveToLibrary(); saved = true; } catch (_) {}
try { rehState.savedId = null; await saveToLibrary(); saved = true; } catch (_) { logErr('rehearsal-save', _); }
}
toast(`Cast ${speakers} character${speakers !== 1 ? 's' : ''} · ${lines} lines` + (saved ? ' — saved to Rehearser → Bibliothek' : ''), 'success');
}
@ -649,7 +649,7 @@ async function audiobookExport(opts = {}) {
const saveUrl = `/api/audiobook-export/${encodeURIComponent(bookForExport)}/${encodeURIComponent(name)}${folderQS}`;
const saveResp = await fetch(saveUrl, { method: 'POST', body: blob });
if (saveResp.ok) savedFiles.push({ name, url: saveUrl });
} catch (_) {}
} catch (_) { logErr('export-server-save', _); }
files++;
await new Promise(r => setTimeout(r, 400));
}

View File

@ -1624,7 +1624,7 @@ async function _audiobookBuildRehRecord(segs) {
// Fetch the existing record and merge its cast so voice assignments survive an update
let existing = null;
if (_audiobook.rehId && typeof window.rehDbGetById === 'function') {
try { existing = await window.rehDbGetById(_audiobook.rehId); } catch (_) {}
try { existing = await window.rehDbGetById(_audiobook.rehId); } catch (_) { logErr('rehearsal-db-lookup', _); }
}
if (existing && existing.cast) {
Object.entries(existing.cast).forEach(([sp, info]) => {

View File

@ -587,7 +587,7 @@
list.innerHTML = '<div class="bench-engine-empty">Checking STT engines...</div>';
let settings = {};
const items = [];
try { settings = await fetchJson('/api/settings'); } catch (_) {}
try { settings = await fetchJson('/api/settings'); } catch (_) { logErr('bench-settings', _); }
try {
const d = await fetchJson('/api/stt-backends');
@ -852,7 +852,7 @@
try {
const settings = await fetchJson('/api/settings');
if (q('bench-turn-llm-url')) q('bench-turn-llm-url').value = settings.conv_llm_url || settings.llm_url || '';
} catch (_) {}
} catch (_) { logErr('bench-turn-settings', _); }
try {
const d = await fetchJson('/api/stt-backends');
const sel = q('bench-turn-stt');

View File

@ -2271,7 +2271,7 @@ async function csShow(sheets, title, sourceText, book, hostEl = null) {
const id = `${bk}::${s.name}`.toLowerCase();
if (imgMap.has(id)) s._image = imgMap.get(id);
});
} catch (_) {}
} catch (_) { logErr('character-sheets-images', _); }
}
// Browsing grid, not the profile itself — use the SAME compact card the
@ -2403,7 +2403,7 @@ async function csAutoGenerateExternalPrompts(book, sheets) {
if (repeatCount >= 3) break;
}
}
try { await clUpsertMany(book, sheets); } catch (_) {}
try { await clUpsertMany(book, sheets); } catch (_) { logErr('character-sheets-save', _); toast('Could not save the generated sheets to the library', 'error'); }
const suffix = failed ? ` (${failed} failed${repeatMsg ? ': ' + repeatMsg.slice(0, 160) : ''})` : '';
toast(`External prompts generated for ${done} character${done !== 1 ? 's' : ''}${suffix}`, failed && !done ? 'error' : 'success');

View File

@ -282,7 +282,7 @@ async function castWriteBack(title, castMap) {
if (curId === voice) continue;
rec.voice = { id: voice };
rec.updated = new Date();
try { await clPut(rec); n++; } catch (_) {}
try { await clPut(rec); n++; } catch (_) { logErr('library-voice-assign', _); }
}
return n;
}

View File

@ -1327,7 +1327,7 @@ async function readerFetchServerAudio(idx) {
try {
const r = await fetch(`${READER_API}/${readerState.savedId}/audio/${idx}`);
if (r.ok) { const b = await r.blob(); readerState.blobCache.set(idx, b); return b; }
} catch (_) {}
} catch (_) { logErr('reader-audio-fetch', _); }
return null;
}
@ -1596,7 +1596,7 @@ async function readerPersistProgress() {
method: 'PATCH', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ idx: readerState.idx, updated: new Date().toISOString() }),
});
} catch (_) {}
} catch (_) { logErr('reader-progress-save', _); }
}
async function readerDeleteLibraryDoc(id) {
@ -1632,7 +1632,7 @@ async function readerOpenLibraryDoc(id) {
if ($('reader-instruct')) $('reader-instruct').value = rec.instruct || '';
// fetch the source document from the server
let sourceBlob = null;
try { const sr = await fetch(`${READER_API}/${id}/source`); if (sr.ok) sourceBlob = await sr.blob(); } catch (_) {}
try { const sr = await fetch(`${READER_API}/${id}/source`); if (sr.ok) sourceBlob = await sr.blob(); } catch (_) { logErr('reader-source-fetch', _); }
try {
if (rec.kind === 'pdf') {
if (!sourceBlob) {

View File

@ -92,7 +92,7 @@ async function saveToLibrary() {
toast('Saved to library', 'success');
rehState._keepSavedIdOnce = true;
// Sync cast voice choices into the shared character roster (existing chars only)
if (typeof castWriteBack === 'function') { try { await castWriteBack(rec.title, rehState.cast); } catch (_) {} }
if (typeof castWriteBack === 'function') { try { await castWriteBack(rec.title, rehState.cast); } catch (_) { logErr('cast-write-back', _); } }
await renderLibraryList();
}

View File

@ -1157,7 +1157,7 @@ async function rehResearchCast(speakers) {
}),
});
if (r.ok) chars = (await r.json()).characters || [];
} catch (_) {}
} catch (_) { logErr('reh-character-analysis', _); }
const byName = {};
chars.forEach(c => { if (c.name) byName[String(c.name).toUpperCase().trim()] = c; });
speakers.forEach(sp => rehWriteCharacterNote(sp, byName[sp.toUpperCase().trim()]));

View File

@ -54,7 +54,7 @@ async function _sfDbGet(key) {
catch (_) { return null; }
}
async function _sfDbPut(rec) {
try { const db = await _sfDbOpen(); await new Promise((res, rej) => { const r = db.transaction(SF_STORE, 'readwrite').objectStore(SF_STORE).put(rec); r.onsuccess = () => res(); r.onerror = e => rej(e.target.error); }); } catch (_) {}
try { const db = await _sfDbOpen(); await new Promise((res, rej) => { const r = db.transaction(SF_STORE, 'readwrite').objectStore(SF_STORE).put(rec); r.onsuccess = () => res(); r.onerror = e => rej(e.target.error); }); } catch (_) { logErr('seed-db-put', _); }
}
async function _sfDbAllForVoice(voiceId) {
try {
@ -75,7 +75,7 @@ async function _sfDbClearVoice(voiceId) {
cur.onsuccess = e => { const c = e.target.result; if (c) { if (c.value.voiceId === voiceId) store.delete(c.primaryKey); c.continue(); } else res(); };
cur.onerror = e => rej(e.target.error);
});
} catch (_) {}
} catch (_) { logErr('seed-db-delete', _); }
}
// Called from voice-inspector.js after the persona panel is appended.
@ -249,7 +249,7 @@ function attachSeedFinder(voiceId, body) {
resultsEl.appendChild(row);
}
} catch (_) {}
} catch (_) { logErr('seed-results-render', _); }
}
// Save (or clear, with seed=null) the voice's seed in the TTS server's voices.json.

View File

@ -255,7 +255,7 @@ async function _statusOpenLlmFlyup(anchor) {
const fetchUrl = '/api/conversation/llm-models?url=' + encodeURIComponent(url);
const data = await fetch(fetchUrl).then(r => r.json());
models = Array.isArray(data.models) ? data.models : [];
} catch (_) {}
} catch (_) { logErr('status-llm-models', _); }
if (!_statusFlyup || _statusFlyup.dataset.forKind !== 'llm') return; // closed/switched while awaiting
_statusFlyupList(anchor, models.map(m => ({ id: m, label: m, active: m === model })), (picked) => {
if (typeof _appSettings !== 'undefined') _appSettings.llm_model = picked;

View File

@ -1797,7 +1797,7 @@ $('lib-add-save').addEventListener('click', async () => {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({voice_id: voiceId, image_url: libAddState.pendingSource.imageUrl})
});
} catch (_) {}
} catch (_) { logErr('voice-picture-save', _); toast('Voice added, but its picture could not be saved', 'error'); }
}
libAddState.pendingSource = null;
setLibAddSourcePreview({});