fix: stabilize async library navigation

This commit is contained in:
mARTin-B78 2026-09-04 08:58:01 +02:00
parent 9637784833
commit fbb5fc52dd
5 changed files with 19 additions and 14 deletions

View File

@ -23,6 +23,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
### Changed
- **API Key Handoff Cache Policy.** Settings and explicit external-key regeneration responses now send `no-store`
headers so browser and intermediary caches cannot retain the client-visible access-key handoff.
- **Navigation Race Protection.** Rehearser phase requests now survive late feature-module initialization, and
overlapping character-library refreshes cannot overwrite a newer table/card view.
- **API Boundary Hardening.** External API authentication is enabled by default, request and mutation limits are
enforced, and settings responses redact provider credentials for non-browser callers.
- **Server-side Provider Credentials.** Provider keys are now masked even for same-origin Settings responses, are

File diff suppressed because one or more lines are too long

View File

@ -4,15 +4,19 @@
// ── Characters / Cast ─────────────────────────────────────────────────────────
let _libCharsRenderSeq = 0;
async function libraryRenderCharacters() {
const container = document.getElementById('lib-chars-list');
if (!container) return;
const renderSeq = ++_libCharsRenderSeq;
// This page can open before the shared voice library has finished loading.
// Ratings live in window._voices, so rendering during that gap makes every
// assigned voice look unrated even when its .meta.json still has a rating.
if ((!Array.isArray(window._voices) || !window._voices.length) && typeof loadVoiceLibrary === 'function') {
try { await loadVoiceLibrary(); } catch (e) { console.warn('[characters] voice library load failed', e); }
if (renderSeq !== _libCharsRenderSeq) return;
}
// Fetch BEFORE touching the DOM: a transient failure here (e.g. the
@ -28,6 +32,7 @@ async function libraryRenderCharacters() {
if (typeof toast === 'function') toast('Failed to load characters — keeping the current view', 'error');
return;
}
if (renderSeq !== _libCharsRenderSeq) return;
// Every action on this page (remove voice, auto-design, delete...) re-runs
// this full rebuild via `refresh()`. Collapsing the container down to a

View File

@ -385,19 +385,16 @@
el.classList.toggle('is-active', Number(el.dataset.rehearserPhase) === phase);
});
// delegate to rehearser's own showPhase() when it's ready
if (typeof showPhase === 'function') {
showPhase(phase);
// Re-assert after the current initialization turn. This is harmless
// when the module was already ready and closes the short race above.
setTimeout(function () {
var applyRequestedPhase = function () {
if (document.getElementById('s-rehearser')?.classList.contains('is-active') && typeof showPhase === 'function') {
showPhase(phase);
}
}, 0);
} else {
// rehearser.js not yet loaded — store the intent and let init pick it up
// from its initialization block.
}
};
// The section loader and feature module can finish after this function is
// called. Retry across their initialization window, including when
// showPhase did not exist on the first call.
applyRequestedPhase();
[0, 50, 250, 500, 1000].forEach(function (delay) { setTimeout(applyRequestedPhase, delay); });
};
// Keep sidebar tree in sync when rehearser changes phase internally

View File

@ -26,6 +26,7 @@ test('Characters / Cast supports profile sorting, column visibility, and voice r
await page.waitForSelector('#lib-chars-list', { state: 'attached', timeout: 15_000 });
await page.waitForFunction(() => typeof window.libraryRenderCharacters === 'function', null, { timeout: 15_000 });
await page.locator('#nav-library-head').evaluate(el => el.click());
await expect(page.locator('#s-library')).toHaveClass(/is-active/, { timeout: 15_000 });
await page.locator('.lib-tab[data-library-view="characters"]').evaluate(el => el.click());
await expect(page.locator('#s-library')).toHaveClass(/is-active/, { timeout: 15_000 });
const tableToggle = page.locator('.lib-chars-view-toggle button[data-view="table"]');