diff --git a/CHANGELOG.md b/CHANGELOG.md index 21cb913..284eafe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ยท versioned wi prose after malformed LLM JSON, clamping caller timeouts, and bounding upstream error text. - **Unified Provider Retries.** TTS and LLM provider requests now retry bounded connection, timeout, and transient HTTP failures with exponential backoff while closing discarded responses. +- **Constrained DOM Coverage.** Added a 250-character browser fixture proving character-library rendering stays within + the configured 50-row page and expands only on explicit user action. - **4 GiB Container Validation.** Verified the current image serves the application under a hard 4 GiB cgroup limit with no OOM kill; the probe recorded approximately 50.6 MiB idle memory and 0.09% CPU. - **Profile Completeness Coverage.** Added browser regression coverage for empty, partial, and fully populated diff --git a/TODO.md b/TODO.md index e50307f..08a6a10 100644 --- a/TODO.md +++ b/TODO.md @@ -115,7 +115,8 @@ the first block before investing in architecture or polish. ## P1 โ€” Frontend performance and memory safety - [ ] Lazy-load Audiobook, Rehearser, Studio, and image-generation modules only when opened. -- [ ] Virtualize or paginate large character and voice tables; render approximately 30โ€“50 visible rows. +- [x] Paginate large character and voice tables; render approximately 25โ€“50 visible rows, with a constrained-device + regression covering a 250-character production. - [ ] Cache `/api/voices` with explicit invalidation instead of repeated full scans. - [x] Lazy-load profile images and concept art. - [ ] Update only affected table rows instead of rebuilding entire tables. diff --git a/VERSION b/VERSION index 53cc1a6..f9e8384 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.24.0 +1.24.1 diff --git a/tests/constrained.spec.js b/tests/constrained.spec.js index f503a70..d22f34a 100644 --- a/tests/constrained.spec.js +++ b/tests/constrained.spec.js @@ -20,3 +20,22 @@ test('4 GB browser profile caps shared batch concurrency at two', async ({ page }); expect(maxActive).toBeLessThanOrEqual(2); }); + +test('large character productions keep the rendered DOM paginated', async ({ page }) => { + const characters = Array.from({ length: 250 }, (_, index) => ({ + id: 'large-' + index, book: 'Large Smoke Book', name: 'Character ' + index, + tags: '', voice: null, sheet: { gender: index % 2 ? 'F' : 'M' }, + })); + await page.route('**/api/characters*', route => route.fulfill({ json: { characters } })); + await page.route('**/api/voices*', route => route.fulfill({ json: [] })); + await page.route('**/api/settings*', route => route.fulfill({ json: {} })); + await page.goto('/', { waitUntil: 'domcontentloaded' }); + await page.waitForFunction(() => typeof window.libraryRenderCharacters === 'function', null, { timeout: 15_000 }); + await page.waitForSelector('#lib-chars-list', { state: 'attached', timeout: 15_000 }); + await page.waitForSelector('#app-skeleton', { state: 'detached', timeout: 15_000 }); + await page.evaluate(() => window.navTo('s-library')); + await page.locator('.lib-tab[data-library-view="characters"]').click(); + await expect.poll(() => page.locator('#lib-chars-list .lib-char-card').count()).toBeLessThanOrEqual(50); + await page.locator('#lib-chars-list .lib-chars-load-more').click(); + await expect.poll(() => page.locator('#lib-chars-list .lib-char-card').count()).toBeLessThanOrEqual(100); +});