From d68cba9432afa38a506937813d0f6543c923b221 Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Fri, 14 Aug 2026 00:14:21 +0200 Subject: [PATCH] fix(file-viewer): make previewed video seekable and stop it on close Two bugs in the File Viewer's media player, both reproduced in a real browser against an 18MB mp4 before and after the fix. 1. Closing the preview left the video playing. closeFilePreview() only dropped the overlay's `visible` class, which is display:none and nothing else, so the audio kept going with no visible player to pause. Detaching the element is not a fix either: a detached HTMLMediaElement plays on until it is garbage collected. _stopFilePreviewMedia() now pauses, drops src and load()s every media element (also on re-open, where overwriting innerHTML had the same effect), which additionally aborts the in-flight download. 2. The scrub bar was inert. file-raw read the whole file and answered 200 with no Accept-Ranges, so Chrome reported video.seekable as [0, 0] and silently reverted `currentTime = x`; Safari refuses to start such media at all. Raw bodies are now streamed and range-aware: Accept-Ranges: bytes on every response, 206 + Content-Range for a Range request, 416 for one past EOF, and a malformed spec ignored (200) per RFC 9110. Parsing is pure in src/web/http-range.ts. Measured on tmp/codeman-crt-v5-66s.mp4 (18MB, 66.6s): before seekable [0, 0] seek to 56.6s reverted to 3.9s close: still playing after seekable [0, 66.56] seek to 56.6s landed at 60.2s close: paused, NETWORK_EMPTY Range slices are byte-identical to `dd`, the full-file path is byte-identical to the file, and the SVG octet-stream/attachment hardening and the 50MB cap are unchanged (the cap is still checked before the range). Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 2 + docs/security-architecture.md | 2 +- src/web/http-range.ts | 86 +++++++++++ src/web/public/panels-ui.js | 37 ++++- src/web/routes/file-routes.ts | 82 ++++++++--- test/file-preview-media.test.ts | 178 ++++++++++++++++++++++ test/http-range.test.ts | 101 +++++++++++++ test/routes/file-routes-range.test.ts | 204 ++++++++++++++++++++++++++ test/routes/file-routes.test.ts | 14 +- 9 files changed, 681 insertions(+), 25 deletions(-) create mode 100644 src/web/http-range.ts create mode 100644 test/file-preview-media.test.ts create mode 100644 test/http-range.test.ts create mode 100644 test/routes/file-routes-range.test.ts diff --git a/CLAUDE.md b/CLAUDE.md index 965566ae..20f32df8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -234,6 +234,8 @@ Codeman is a Claude Code session manager with web interface and autonomous Ralph **File Viewer edit mode** (issue #212): the file-preview overlay edits workspace text files in place — `GET .../file-content?edit=1` + `PUT /api/sessions/:id/file-content`, policy in `src/config/file-editing.ts`. This is a **third file surface and the only one that WRITES**: read-path confinement (realpath + workspace + ownership) plus sensitive/blocked/`.git` denies and an extension **allowlist**; writes are `wx`-temp + rename (no `O_CREAT` anywhere = edit-in-place is structural); optimistic concurrency via sha256 `baseHash` → 409. ⚠️ `edit=1` never truncates and the client must never save a plain-preview buffer (the 500-line truncation would silently delete the rest). ⚠️ CRLF/UTF-8 guards: EOL re-applied server-side, non-UTF-8 refused via round-trip compare. → [architecture-invariants#file-viewer-edit-mode](docs/architecture-invariants.md#file-viewer-edit-mode), `docs/file-viewer-edit-plan.md` +**Raw file bodies are streamed and range-aware**: `file-raw` and the attachments `/raw` route always advertise `Accept-Ranges: bytes` and answer a `Range` header with `206` + `Content-Range` (single-range only; parser is pure + unit-tested in `src/web/http-range.ts`, a malformed spec is ignored → 200 while an out-of-bounds one is a 416). ⚠️ A 200-only response is what made the File Viewer's `