fix(ui): surface silently swallowed save and fetch errors
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2479617964
commit
60eb108360
@ -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`.
|
||||
|
||||
10
static/dist/main.min.js
vendored
10
static/dist/main.min.js
vendored
File diff suppressed because one or more lines are too long
@ -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();
|
||||
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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 (_) {}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
@ -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]) => {
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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');
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -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()]));
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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({});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user