diff --git a/TODO.md b/TODO.md index 7fe168e..e414e62 100644 --- a/TODO.md +++ b/TODO.md @@ -86,10 +86,11 @@ the first block before investing in architecture or polish. - [x] Establish a Playwright baseline before large refactors (app shell, bundle, i18n, clone tabs, and Rehearser parsing; installed Chromium supported). - [x] Add explicit Docker memory limits where appropriate; the web container defaults to an 8 GB limit and 2 GB reservation with environment overrides. -- [ ] Limit upload sizes, temporary files, and concurrent jobs. +- [x] Limit upload sizes, temporary files, and concurrent jobs. Upload/request caps, bounded caches, and bounded + TTS/STT/image-generation pools are implemented; durable background-job conversion remains a separate P1 item. - [x] Use atomic writes for `.meta.json` files. -- [ ] Create automatic backups before bulk deletes, voice moves, and recasts. -- [ ] Add request-size limits, rate limits, and authentication for destructive/API endpoints. +- [x] Create automatic backups before bulk deletes, voice moves, and recasts. +- [x] Add request-size limits, rate limits, and authentication for destructive/API endpoints. ## P1 — Modular frontend architecture @@ -120,21 +121,23 @@ the first block before investing in architecture or polish. - [ ] Introduce background jobs for voice design, transcription, casting, and image generation. - [ ] Give every job an ID, progress, cancellation, timeout, retry, and idempotency key. -- [ ] Prevent duplicate generation when a user clicks twice. +- [x] Prevent duplicate generation when a user clicks twice for image, voice-design, and casting submissions. - [ ] Centralize provider health checks and error handling. -- [ ] Limit concurrency to 1–2 image generations, 1–2 voice generations, and 1 STT request per user. Consistent +- [x] Limit concurrency to 1–2 image generations, 1–2 voice generations, and 1 STT request per user. Consistent portrait/reference rendering, preview/style/seed-finder synthesis, and the OpenAI-compatible speech proxy are capped at two active jobs; STT transcription and benchmarks are capped at one active job. -- [ ] Use bounded server queues. +- [x] Use bounded server queues for the implemented synchronous generation paths; durable background-job queues remain + a separate outstanding item above. ## P0 — Security - [ ] Never expose API keys to frontend JavaScript or logs. Provider keys are masked on read and absent from browser storage; the deliberate external-client key display and transient entry path still need a final design review. -- [ ] Validate uploaded filenames and paths. -- [ ] Strictly validate custom URLs to prevent SSRF. -- [ ] Add request-size limits, rate limits, and authentication for destructive/API endpoints. -- [ ] Run containers as non-root with dropped capabilities and read-only filesystems where practical. +- [x] Validate uploaded filenames and paths. +- [x] Strictly validate custom URLs to prevent SSRF, with private destinations available only when explicitly allowed + for local provider/container operation. +- [x] Add request-size limits, rate limits, and authentication for destructive/API endpoints. +- [x] Run containers as non-root with dropped capabilities and read-only filesystems where practical. - [x] Isolate Docker socket access behind a minimal privileged service. ## P0 — Data integrity and recovery diff --git a/tests/rehearser.spec.js b/tests/rehearser.spec.js index 0585c24..83d91e9 100644 --- a/tests/rehearser.spec.js +++ b/tests/rehearser.spec.js @@ -24,6 +24,9 @@ async function navTo(page, section) { } test('parses a screenplay and builds the cast', async ({ page }) => { + await page.route('**/api/voices*', route => route.fulfill({ + json: [{ id: 'smoke_voice', name: 'Smoke voice', enabled: true }], + })); await page.goto('/', { waitUntil: 'domcontentloaded' }); await navTo(page, 's-rehearser'); await page.waitForFunction(() => typeof window.navRehearserPhase === 'function', null, { timeout: 15_000 }); diff --git a/tests/smoke.spec.js b/tests/smoke.spec.js index 219a015..32ebedf 100644 --- a/tests/smoke.spec.js +++ b/tests/smoke.spec.js @@ -47,12 +47,17 @@ test('clone tabs switch and show one panel at a time', async ({ page }) => { await page.goto('/', { waitUntil: 'domcontentloaded' }); await navTo(page, 's-clone'); await page.waitForFunction(() => typeof window._cloneScheduleAutoSave === 'function', null, { timeout: 15_000 }); - await expect(page.locator('.clone-src-tab[data-src="url"]')).toBeVisible({ timeout: 10_000 }); + // The loader applies the persisted startup section during its final reveal; + // wait for that initialization to finish before changing sections. + await page.waitForSelector('#app-skeleton', { state: 'detached', timeout: 15_000 }); + await page.evaluate(() => window.navTo('s-clone')); + const clone = page.locator('#s-clone'); + await expect(clone.locator('.clone-src-tab[data-src="url"]')).toBeVisible({ timeout: 10_000 }); // The app uses a fixed bottom status bar; dispatching the real DOM click // avoids a viewport-dependent overlap while still exercising the handler. - await page.locator('.clone-src-tab[data-src="url"]').evaluate((el) => el.click()); - await expect(page.locator('#clone-src-url')).toBeVisible(); - await expect(page.locator('#clone-src-mic')).toBeHidden(); + await clone.locator('.clone-src-tab[data-src="url"]').evaluate((el) => el.click()); + await expect(clone.locator('#clone-src-url')).toBeVisible(); + await expect(clone.locator('#clone-src-mic')).toBeHidden(); }); test('PWA manifest and service worker are served', async ({ page, request }) => {